moonflac

    Pure MoonBit FLAC encoding and decoding for native, JavaScript and WebAssembly GC

    flac
    audio
    decoder
    encoder
    streaming
    Download zip
    Author
    Version
    0.2.1
    License
    Apache-2.0
    Last updated
    2 hours ago
    Downloads
    3

    #MoonFLAC

    MoonFLAC is a pure MoonBit lossless FLAC codec for native, JavaScript and WebAssembly GC. It covers decoding, native FLAC encoding, Ogg FLAC muxing, streaming, exact sample cropping, a CLI and a browser playground. The codec library uses no libFLAC, FFmpeg, FFI or browser audio decoder; libFLAC is only an external validation oracle.

    #Features

    AreaCoverage
    PCM4–32 valid bits, 1–8 channels, 1–1,048,575 Hz
    FramesFixed/variable blocks, 16–65,535 samples, CRC8 and CRC16
    SubframesConstant, verbatim, fixed 0–4, LPC 1–32, Rice/Rice2, escape, wasted bits
    ArithmeticInt64 prediction and stereo reconstruction, including 33-bit side channels
    MetadataSTREAMINFO, PADDING, APPLICATION, SEEKTABLE, VORBIS_COMMENT, CUESHEET, PICTURE; unknown 7–126 retained
    IntegrityFrame CRCs before output; available STREAMINFO MD5 checked at stream/link end
    ContainersNative/Ogg FLAC decode with chained links; Ogg FLAC muxing from native frames
    AccessWhole-file and incremental decode, frame index, half-open sample cropping
    OutputRaw PCM, WAV, WAVE_FORMAT_EXTENSIBLE and RF64
    EncodingNative FLAC from 4–32-bit PCM; whole-buffer and bounded streaming APIs; effort 0–8; Ogg output

    The implementation follows RFC 9639, including verified errata 8256 and 8810.

    #Build and test

    Install the MoonBit toolchain, a native C toolchain, Node.js and Python 3. CI pins compiler 0.10.19+97801ee1b.

    Install the published library:

    moon add 0717lee/moonflac@0.2.1

    moon update moon fmt --check for target in native js wasm-gc; do moon check --target "$target" moon test --target "$target" moon build --target "$target" moon run examples/stream --target "$target" moon run examples/encode --target "$target" moon run examples/ogg --target "$target" done

    The root library has no platform FFI. The CLI is native; the browser bridge targets JavaScript and wasm-gc.

    #CLI

    moon run cmd/moonflac --target native -- info recording.flac --json moon run cmd/moonflac --target native -- verify recording.flac moon run cmd/moonflac --target native -- decode recording.flac output.wav moon run cmd/moonflac --target native -- decode recording.flac excerpt.wav --start 48000 --end 96000 moon run cmd/moonflac --target native -- encode recording.wav output.flac moon run cmd/moonflac --target native -- encode input.pcm output.flac --raw --sample-rate 48000 --channels 6 --bits 24 --channel-mask 0x060f moon run cmd/moonflac --target native -- ogg output.flac output.oga

    Sample ranges are zero-based [start, end), per channel. Exit codes: 0 success, 2 usage, 3 invalid input/resource limit, 4 file I/O. All commands accept --max-buffer BYTES (default 32 MiB).

    encode reads integer WAV/RF64. Raw input requires sample rate, channels and bit depth. --block-size defaults to 4096, --effort to 5, --variable-block enables adaptive blocking.

    #Ogg FLAC output

    ogg_from_flac(native) validates one complete native FLAC stream and writes one Ogg FLAC logical stream, preserving STREAMINFO and metadata. encode_ogg_pcm(samples, config) is the whole-buffer PCM-to-Ogg path.

    moonflac ogg recording.flac recording.oga moonflac info recording.oga

    --serial HEX sets the logical-stream serial; --vendor NAME sets the vendor only when the source has no Vorbis comment. The CLI stages its input in memory; the native Encoder remains the bounded streaming path. examples/ogg is a three-target round trip.

    #Library and streaming

    Import 0717lee/moonflac as @flac. decode(bytes) returns one stream; decode_chained(bytes) returns Ogg links separately. For long input, use Decoder::new(), push, next and finish; consume each frame before reading more data. API documentation defines ownership, errors, integrity timing and resource limits.

    encode_pcm(samples, EncoderConfig::new(format)) returns finalized native FLAC. Encoder provides bounded streaming with retained PCM capped at block_size * channels. OggDemux provides incremental Ogg input.

    #Browser playground

    node playground/build.mjs node playground/serve.mjs

    Open http://127.0.0.1:8080, choose JavaScript or WebAssembly GC, inspect metadata/waveform, play/pause/seek and export WAV. The MoonBit artifact runs in a Worker; integer WAV export preserves source precision. See playground details.

    #Verification

    • Native, JavaScript and wasm-gc: 197/197 project tests each.
    • CI: three-target check/test/build, CLI contract tests, libFLAC PCM comparison and browser build.
    • Clean clone: three targets 197/197, CLI contract test passes.
    • Ogg output: libFLAC -t accepted; 16/24/32-bit, 2/6/8-channel PCM matches byte-for-byte.
    • Historical decoder/encoder/long-stream evidence remains summarized in docs/ACCEPTANCE.md.

    libFLAC is the independent oracle; MoonFLAC does not use its own decoder as reference. The performance goal of beating libFLAC is not met.

    #Boundaries

    The ogg CLI stages its input in memory. A bounded Ogg streaming writer, MP4/Matroska mappings, OS player backends and AudioWorklet playback are out of scope. Resource limits are explicit errors and can be raised for unusually large valid inputs.

    Apache-2.0. See LICENSE and THIRD_PARTY.md. This checkout is the public submission; see HACKATHON.md.

    FlacError

    pub suberror FlacError {
    NeedMore
    Invalid(Int64, String)
    Limit(String)
    } derive(Show)

    Positioned format errors, incomplete input, and explicit resource limits.

    Audio

    pub(all) struct Audio {
    metadata : Metadata
    frames : Array[Frame]
    integrity : Integrity
    }

    Whole-file convenience API. Frame buffers are owned by the returned value.

    CueIndex

    pub(all) struct CueIndex {
    offset : UInt64
    number : Int
    } derive(Show)

    CueSheet

    pub(all) struct CueSheet {
    catalog : Bytes
    lead_in : UInt64
    is_cd : Bool
    tracks : Array[CueTrack]
    } derive(Show)

    CueTrack

    pub(all) struct CueTrack {
    offset : UInt64
    number : Int
    isrc : Bytes
    non_audio : Bool
    pre_emphasis : Bool
    indices : Array[CueIndex]
    } derive(Show)

    DecodeEvent

    pub(all) enum DecodeEvent {
    MetadataReady(Metadata)
    AudioFrame(Frame)
    LinkEnd(Integrity)
    NeedInput
    Done(Integrity)
    }

    Pull one event at a time; PCM arrays belong to the caller.

    Decoder

    type Decoder

    An incremental decoder. push accepts a prefix; drain next before retrying the remainder. finish declares EOF, then next validates length and MD5.

    Decoder::buffered_bytes

    fn Decoder::buffered_bytes(self : Decoder) -> Int

    Current retained compressed native bytes (not output PCM).

    Decoder::compressed_storage_bytes

    fn Decoder::compressed_storage_bytes(self : Decoder) -> Int64

    Native compressed staging plus the Ogg page and partial packet, if present.

    Decoder::finish

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

    Declare end of input. Call next until Done or an explicit error.

    Decoder::new

    fn Decoder::new(max_buffer_bytes? : Int) -> Decoder

    max_buffer_bytes limits retained compressed native bytes; Ogg also bounds its partial packet to this value. A caller can raise it for large metadata.

    Decoder::next

    fn Decoder::next(self : Decoder) -> DecodeEvent raise FlacError

    Errors are terminal and repeated on later calls.

    Decoder::peak_buffered_bytes

    fn Decoder::peak_buffered_bytes(self : Decoder) -> Int

    Peak retained native compressed bytes, for resource accounting.

    Decoder::peak_compressed_storage_bytes

    fn Decoder::peak_compressed_storage_bytes(self : Decoder) -> Int64

    Conservative upper bound on peak retained compressed bytes. Independent native and Ogg peaks can occur at different times; PCM is caller-owned.

    Decoder::push

    fn Decoder::push(self : Decoder, input : Bytes) -> Int raise FlacError

    Supply input and return the number of bytes accepted. Input is copied.

    EncodeEvent

    pub(all) enum EncodeEvent {
    EncodedBytes(Bytes)
    NeedPcm
    EncoderDone(EncoderSummary)
    }

    Each emitted byte chunk is owned and is never reused by the encoder.

    Encoder

    type Encoder

    Bounded push/pull encoder. Input is interleaved signed scalar samples. Internal PCM never exceeds block_size * channels. Drain next after push; an accepted count of zero means the pending output/block must be drained.

    Encoder::buffered_samples

    fn Encoder::buffered_samples(self : Encoder) -> Int

    Number of scalar PCM values retained, excluding caller-owned output.

    Encoder::finish

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

    Declares PCM EOF. Empty streams and incomplete interchannel groups fail.

    Encoder::new

    fn Encoder::new(config : EncoderConfig) -> Encoder raise FlacError

    Encoder::next

    fn Encoder::next(self : Encoder) -> EncodeEvent raise FlacError

    Pull owned compressed bytes. Initial bytes contain unknown streaming STREAMINFO fields; EncoderDone returns the same-length finalized header.

    Encoder::peak_buffered_samples

    fn Encoder::peak_buffered_samples(self : Encoder) -> Int

    Peak retained scalar values; bounded by configured block_size * channels.

    Encoder::push

    fn Encoder::push(self : Encoder, samples : Array[Int]) -> Int raise FlacError

    Accepted scalar count; each accepted sample is copied and range checked. Partial interchannel groups may span pushes, but must be complete at finish.

    EncoderConfig

    pub(all) struct EncoderConfig {
    format : PcmFormat
    block_size : Int
    variable_block : Bool
    effort : Int
    channel_mask : UInt?
    metadata : Array[MetadataBlock]
    non_audio : Bool
    max_metadata_bytes : Int
    }

    Effort bounds candidate searches, never the accepted PCM domain.

    EncoderConfig::new

    fn EncoderConfig::new(format : PcmFormat, block_size? : Int, variable_block? : Bool, effort? : Int, channel_mask? : UInt, metadata? : Array[MetadataBlock], non_audio? : Bool, max_metadata_bytes? : Int) -> EncoderConfig

    EncoderSummary

    pub(all) struct EncoderSummary {
    total_samples : Int64
    frames : Int64
    min_frame_size : Int
    max_frame_size : Int
    md5 : Bytes
    final_header : Bytes
    }

    final_header replaces the original same-length header for seekable output. Non-seekable output remains valid with zero unknown totals/digest fields.

    Frame

    pub(all) struct Frame {
    sample_number : Int64
    number : Int64
    variable_block : Bool
    sample_rate : Int
    bits_per_sample : Int
    channels : Array[Array[Int]]
    block_size : Int
    byte_offset : Int64
    byte_length : Int
    } derive(Show)

    Owned planar signed PCM, unshifted at its original bit depth.

    FrameLocation

    pub(all) struct FrameLocation {
    sample : Int64
    offset : Int64
    samples : Int
    bytes : Int
    }

    A verified native frame location. Offsets refer to the original input.

    Integrity

    pub(all) enum Integrity {
    Md5Verified
    Md5Unavailable
    FrameCrcOnly
    } derive(Eq, Show,
    Debug
    )

    STREAMINFO MD5 validation at the end of a complete stream.

    Md5

    type Md5

    Incremental RFC 1321 state; only one partial block is retained.

    Metadata

    pub(all) struct Metadata {
    info : StreamInfo
    blocks : Array[MetadataBlock]
    seek_points : Array[SeekPoint]
    comments : VorbisComment?
    cuesheet : CueSheet?
    cuesheets : Array[CueSheet]
    pictures : Array[Picture]
    } derive(Show)

    Metadata::channel_mask

    fn Metadata::channel_mask(self : Metadata) -> UInt? raise FlacError

    Read the optional RFC 9639 channel-layout tag. None means the standard FLAC ordering; Some(0) explicitly leaves every channel's speaker undefined. Fewer named speakers than audio channels is legal and is preserved. Repeated equivalent tags are accepted; conflicting tags are an error.

    Metadata::effective_channel_mask

    fn Metadata::effective_channel_mask(self : Metadata) -> UInt raise FlacError

    Resolve absent layout metadata to the standard FLAC channel assignment. Explicit zero and partial assignments remain unchanged.

    MetadataBlock

    pub(all) struct MetadataBlock {
    kind : Int
    data : Bytes
    } derive(Show)

    Raw blocks are retained, including reserved types 7..126.

    OggDemux

    type OggDemux

    Incremental Ogg FLAC demultiplexer. Output owns its bytes. Chained streams emit a fresh fLaC signature, allowing callers to finish/start decoders. After an error, discard this demultiplexer rather than resuming it.

    OggDemux::buffered_bytes

    fn OggDemux::buffered_bytes(self : OggDemux) -> Int

    OggDemux::finish

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

    OggDemux::new

    fn OggDemux::new(max_packet_bytes? : Int) -> OggDemux

    OggDemux::peak_buffered_bytes

    fn OggDemux::peak_buffered_bytes(self : OggDemux) -> Int

    OggDemux::push

    fn OggDemux::push(self : OggDemux, data : Bytes) -> Array[Bytes] raise FlacError

    Consumes arbitrarily large caller input a byte at a time without retaining it.

    PcmFormat

    pub(all) struct PcmFormat {
    sample_rate : Int
    channels : Int
    bits_per_sample : Int
    } derive(Eq,
    Debug
    )

    Signed unshifted integer PCM format; a sample is one channel value.

    PcmInput

    pub struct PcmInput {
    pending : BytesView
    offset : Int64
    ended : Bool
    failed : FlacError?
    stage : Int
    format : PcmFormat?
    announced : Bool
    mask : UInt?
    width : Int
    shift : Int
    raw : Bool
    rf64 : Bool
    riff_end : Int64?
    chunk_left : Int64
    pad : Bool
    saw_data : Bool
    saw_ds64 : Bool
    data_size : Int64
    data_size_used : Bool
    expected_samples : Int64
    samples : Int64
    table_left : Int
    sizes : Map[UInt, Array[Int64]]
    size_positions : Map[UInt, Int]
    }

    A bounded RIFF/RF64 or signed raw PCM reader. Unknown chunks are skipped incrementally. RF64 size tables have an explicit 65536-entry resource limit.

    PcmInput::buffered_bytes

    fn PcmInput::buffered_bytes(self : PcmInput) -> Int

    PcmInput::finish

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

    PcmInput::next

    fn PcmInput::next(self : PcmInput) -> PcmInputEvent raise FlacError

    Pull a format, complete interleaved PCM group, input request or completion. Errors are sticky, and truncated input is diagnosed after finish.

    PcmInput::push

    fn PcmInput::push(self : PcmInput, input : Bytes) -> Int raise FlacError

    Copies at most the remaining 64 KiB staging capacity; drain next and retry any unaccepted suffix. Calling after finish is an error.

    PcmInput::raw

    fn PcmInput::raw(format : PcmFormat) -> PcmInput raise FlacError

    Raw PCM is signed little-endian, sign-extended and unshifted in ceil(bits/8) bytes. Zero sample rate is representable; the encoder requires non_audio.

    PcmInput::wav

    fn PcmInput::wav() -> PcmInput

    Read integer RIFF WAVE or RF64; floating-point PCM is explicitly rejected.

    PcmInputEvent

    pub(all) enum PcmInputEvent {
    PcmFormatReady(PcmFormat, UInt?)
    PcmSamples(Array[Int])
    PcmNeedInput
    PcmInputDone
    }

    Incremental integer PCM input events. Samples are interleaved, unshifted, owned, and always contain complete channel groups.

    Picture

    pub(all) struct Picture {
    kind : UInt
    media_type : String
    description : String
    width : UInt
    height : UInt
    depth : UInt
    colors : UInt
    data : Bytes
    } derive(Show)

    RawDecodeEvent

    pub(all) enum RawDecodeEvent {
    RawFrame(Frame)
    Skipped(Int64, Int)
    RawNeedInput
    RawDone
    }

    Raw streams have no metadata or MD5. Skipped reports every discarded byte, including initial garbage and damaged frames; it is never silent recovery.

    RawDecoder

    type RawDecoder

    Decoder for independently framed FLAC (RFC 9639 section 9). Format changes are allowed. Optional info supplies the values represented by header code 0; they are not inferred from earlier frames. Returned frame sample_number is the local count of successfully decoded samples; number retains the original encoded number. The receiver must use each frame's rate/depth/channels. Incomplete frames are retried at geometrically increasing buffer sizes. This bounds repeated parsing for tiny input chunks, with up to the current partial-frame length of lookahead latency. finish always forces a retry.

    RawDecoder::buffered_bytes

    fn RawDecoder::buffered_bytes(self : RawDecoder) -> Int

    RawDecoder::finish

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

    Declare EOF. Drain next to RawDone; incomplete candidates raise an error.

    RawDecoder::new

    fn RawDecoder::new(info? : StreamInfo, max_buffer_bytes? : Int) -> RawDecoder

    RawDecoder::next

    fn RawDecoder::next(self : RawDecoder) -> RawDecodeEvent raise FlacError

    Header CRC8 and complete frame CRC16 must pass before any PCM is emitted. Every resynchronization is reported. A truncated candidate at EOF is an error; a fully rejected candidate is reported as discarded bytes.

    RawDecoder::peak_buffered_bytes

    fn RawDecoder::peak_buffered_bytes(self : RawDecoder) -> Int

    RawDecoder::push

    fn RawDecoder::push(self : RawDecoder, input : Bytes) -> Int raise FlacError

    Copy an input prefix, returning accepted bytes. Drain next before retrying.

    SeekIndex

    pub(all) struct SeekIndex {
    metadata : Metadata
    frames : Array[FrameLocation]
    total_samples : Int64
    integrity : Integrity
    }

    Build once, reuse against the same immutable native FLAC input.

    SeekIndex::locate

    fn SeekIndex::locate(self : SeekIndex, sample : Int64) -> FrameLocation?

    Find the last indexed frame at or before sample, in logarithmic time.

    SeekPoint

    pub(all) struct SeekPoint {
    sample : UInt64
    offset : UInt64
    samples : Int
    } derive(Show)

    StreamInfo

    pub(all) 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
    } derive(Show)

    STREAMINFO values. A zero total_samples or all-zero MD5 means unknown.

    VorbisComment

    pub(all) struct VorbisComment {
    vendor : String
    entries : Array[String]
    } derive(Show)

    VorbisComment::values

    fn VorbisComment::values(self : VorbisComment, name : String) -> Array[String]

    Return all values of a tag name, comparing printable ASCII names without case.

    build_index

    fn build_index(data : Bytes) -> SeekIndex raise FlacError

    Validate a native stream and retain frame locations, not decoded PCM.

    crop_frame

    fn crop_frame(frame : Frame, start : Int64, end : Int64) -> Frame?

    Crop a frame to the half-open absolute sample interval. Copies all planes.

    decode

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

    Decode native FLAC or Ogg FLAC with complete integrity validation.

    decode_chained

    fn decode_chained(data : Bytes) -> Array[Audio] raise FlacError

    Decode every logical FLAC link of an Ogg chain, or one native stream.

    decode_indexed_range

    fn decode_indexed_range(data : Bytes, index : SeekIndex, start : Int64, end : Int64) -> Audio raise FlacError

    Seek through an existing index. The index must describe this exact input; every selected frame CRC is checked again. Global MD5 comes from build_index.

    decode_range

    fn decode_range(data : Bytes, start : Int64, end : Int64) -> Audio raise FlacError

    Exact half-open interval, with whole-stream integrity verification. Native FLAC uses a frame index; Ogg is demultiplexed sequentially.

    decode_seektable_range

    fn decode_seektable_range(data : Bytes, start : Int64, end : Int64) -> Audio raise FlacError

    Decode a half-open sample interval using native SEEKTABLE offsets. Only the selected frames are CRC-verified; STREAMINFO MD5 cannot be verified without decoding the whole stream. The result explicitly reports FrameCrcOnly. A missing usable point starts at the first frame. Invalid selected points are errors, never silently ignored or recovered by scanning for sync bytes. An empty interval reads metadata only; if total_samples is unknown, its endpoint is not proven to lie inside the stream. Use build_index for that.

    encode_ogg_pcm

    fn encode_ogg_pcm(samples : Array[Int], config : EncoderConfig, serial? : UInt, vendor? : String) -> Bytes raise FlacError

    Encode interleaved integer PCM and wrap the result in Ogg FLAC. This is the whole-buffer convenience API; native streaming remains available separately.

    encode_pcm

    fn encode_pcm(samples : Array[Int], config : EncoderConfig) -> Bytes raise FlacError

    Whole-buffer convenience API. Interleaved PCM is not modified. The returned native FLAC has final STREAMINFO counts, frame bounds and source PCM MD5.

    frame_pcm

    fn frame_pcm(frame : Frame, wav? : Bool) -> Bytes

    Serialize owned interleaved PCM. Raw PCM is signed, little endian and sign-extended. WAV PCM is left-aligned and its 8-bit container is unsigned.

    ogg_from_flac

    fn ogg_from_flac(native : Bytes, serial? : UInt, vendor? : String) -> Bytes raise FlacError

    Convert a complete native FLAC file into one Ogg FLAC logical stream. The native stream is validated by build_index before its frames are repackaged, so corrupt input cannot silently become a valid Ogg file.

    parse_metadata

    fn parse_metadata(data : Bytes, max_metadata_bytes? : Int) -> (Metadata, Int) raise FlacError

    Parse native metadata; incomplete input raises NeedMore. Limits apply before copying payloads.

    wav_header

    fn wav_header(info : StreamInfo, samples : Int64, channel_mask? : UInt, rf64? : Bool) -> Bytes raise FlacError

    Header only, allowing RF64 size planning without PCM allocation. Append exactly samples * channels * ceil(bits/8) data bytes and, when odd, one zero pad byte. The pad is included in RIFF/ds64 size but excluded from data size.

    Powered by MoonBit

    Site sourceReport issueModulesSkillsBuild queueStatistics

    © 2026 mooncakes.io