Skip to content

Releases: CrispStrobe/glint

glint v0.9.0 — the three-codec release

Choose a tag to compare

@github-actions github-actions released this 11 Jul 17:55

glint v0.9.0 — the three-codec release

72 commits since v0.8.0. glint is no longer a dual-codec encoder: it is
now a three-codec suite — MP3, AAC-LC and Opus — that both encodes and
decodes
, under a single MIT license, entirely clean-room, with one CLI
and Python / Rust / Dart bindings over the whole thing. Every codec is
complete in both directions; the Opus codec is new in this release, and
both the MP3 and AAC decoders are new. Every claim below is measured and
gated in CI; the full experiment log — including the dead ends — lives in
PLAN.md.

Headline

  • Opus, from scratch (RFC 6716 / 7845 / 8251). A clean-room,
    RFC-conformant decoder (12/12 official test vectors, verified by the
    normative opus_compare) — SILK, CELT and hybrid, with PLC, SILK in-band
    FEC, output gain, non-48 kHz output rates, and multistream/surround — plus
    a competitive CELT encoder validated on every frame by libopus's own
    decoder. Ogg .opus read and write. In the 10-clip PEAQ ODG league the
    glint encoder is transparent at 192 kbps everywhere and at 96 kbps
    wins or ties most clips against libopus.
  • MP3 and AAC-LC decoders, from scratch. Clean-room MPEG-1/2 Layer III
    (all window types, reservoir, M/S + intensity stereo) and AAC-LC (all four
    window sequences, M/S, TNS, PNS, intensity) decoders — each built from the
    encoder's own Huffman tables, so encode and decode cannot drift. The MP3
    decoder matches ffmpeg to 128–131 dB across 19 streams (LAME included);
    the AAC decoder matches ffmpeg and Apple CoreAudio, and its FFT-based
    IMDCT decodes at ~440× realtime.
  • A codec Swiss-army-knife CLI. glint_cli now encodes, decodes and
    transcodes MP3 / AAC / Opus over a universal float-PCM pipeline, with
    WAV/raw I/O at any bit depth, resampling, gain, peak-normalize, --info,
    and stdin/stdout piping.
  • Bindings for everything. Python, Rust and Dart wrappers now cover
    decode, transcode, resample, WAV I/O and one-call encode for all three
    codecs — not just MP3/AAC encode.

The Opus codec

Built bottom-up from the RFC in gated milestones (PLAN.md § O0–O5):

  • Range coder byte-identical to libopus (the foundation everything keys
    on: complemented decoder value, raw bits filled from the buffer end,
    0xFF carry chains, Q15 tell_frac).
  • CELT decoder — energy-envelope decode, bit allocator, PVQ shapes,
    mixed-radix inverse MDCT, anti-collapse — and CELT encoder: prefilter,
    transient detection with short blocks, tf_analysis, alloc_trim,
    dynalloc with wire boosts, spread/tapset, intensity + dual-stereo,
    unconstrained VBR, and FFT-phase tonality analysis (no MLP). libopus's
    decoder verifies our final range on every encoded frame.
  • SILK decoder — bit-exact: NLSF→LPC chain, LTP, stereo unmix,
    excitation shell coder — completing the decoder to full RFC conformance.
  • O5 polish: SILK in-band FEC decode, output rates 8–48 kHz, output
    gain, and multistream/surround (mapping family 1, 5.1 + quad) decode.

The whole O0–O5 roadmap is done. Each layer has its own crosscheck gate
against libopus; the decoder passes all 12 official vectors, and the encoder
gate is libopus decoding glint's streams with a per-packet range identity.

MP3 + AAC-LC decoders

Both decoders complete their codec in the decode direction, so glint is now
symmetric on all three:

  • MP3 (D1): MPEG-1 and MPEG-2 LSF, bit reservoir, all block/window
    types, M/S and intensity stereo, polyphase synthesis. Gated at 19/19 vs
    ffmpeg, 128–131 dB
    (including LAME streams and hand-built intensity
    frames no encoder emits).
  • AAC-LC (D2): ADTS, all four window sequences, M/S, TNS, PNS
    (codebook 13), intensity (14/15). glint round-trips at 86–135 dB by
    SNR
    ; foreign ffmpeg/FDK/Apple streams (whose PNS is decoder-random) match
    in the spectral-envelope domain. The direct O(N²) IMDCT was replaced by
    inverting the encoder's proven MDCT — 403× faster, ~440× realtime,
    reconstruction-identical.
  • Dual-reference validation: the AAC decoder is checked against ffmpeg
    and Apple CoreAudio, which decode glint's streams metrically identical.

CLI + bindings

The CLI is now a general codec tool over one interleaved-float pipeline:

glint_cli in.wav  out.opus          # encode (mp3 | aac | opus)
glint_cli in.mp3  out.wav           # decode any codec -> WAV/raw
glint_cli in.mp3  out.aac           # transcode
glint_cli --info in.opus            # format / rate / channels / duration
… | glint_cli -F mp3 - -  | …       # stdin/stdout piping
  --rate HZ  --gain DB  --norm[=DB]  --bits 8|16|24|32  --wav-float

WAV I/O reads PCM 8/16/24/32, IEEE float 32/64, A-law, µ-law and
WAVE_FORMAT_EXTENSIBLE, and writes any of those depths. Opus output
auto-resamples to 48 kHz; the resampler is a dependency-free Kaiser-windowed
sinc.

Bindings (Python / Rust / Dart) gained, over a shared C ABI:
decode_audio / decode_audio_ex (whole-file auto-detect, output rate,
int16-or-float output, Opus surround up to 8 ch), encode_audio (one call,
auto-resamples to a codec-valid rate), wav_read / wav_write at any bit
depth, resample, and Opus file encode. The low-level encoders expose the
full config (MP3 mode/quality/VBR, AAC VBR). Each addition ships with unit +
live round-trip tests wired into ctest.

Robustness and security

glint's entire job is parsing untrusted compressed audio, so the decode
surface is fuzzed under sanitizers as a CI gate (decoder_fuzz):

  • Frame decoders (MP3 / AAC under ASan+UBSan, Opus under ASan) survive
    random, bit-flipped and truncated input with no crash, OOB or hang. This
    found and fixed four AAC decoder bugs on malformed input (an infinite
    loop, a heap overflow, a stack overflow, and an OOB read).
  • Container parsers (the WAV reader and the Ogg-Opus demuxer) are now
    fuzzed too. A parser-hardening audit found and fixed a WAV
    WAVE_FORMAT_EXTENSIBLE out-of-bounds read
    (a truncated fmt chunk
    claiming a 40-byte size read past the buffer); the Ogg demuxer audited
    clean.

Compatibility

  • Additive and backward-compatible. All v0.8.0 APIs are unchanged; the
    new decode/transcode/resample/WAV/Opus entry points are additions to the C
    ABI and the three wrappers.
  • Builds: double (default), fixed (integer, no-FPU MP3/AAC hot
    paths), and both (runtime -p) all pass CI on Linux and macOS. The
    embedded/no-FPU encoder core is unchanged — the new whole-file convenience
    code is desktop-only and not compiled into the microcontroller build.
  • ctest is now 8 gates: unit tests, MPEG-2 decode quality, AAC encode
    quality, MP3 and AAC decoder-vs-ffmpeg, the CLI feature gate, the Python
    bindings, and the decoder+container fuzz gate.

The full per-item history and every measured result (Opus O0–O5, the MP3/AAC
decoders, the CLI/wrapper flexibility pass, and the parser audit) is in
PLAN.md.

glint v0.8.0 — the AAC release

Choose a tag to compare

@github-actions github-actions released this 06 Jul 14:50

glint v0.8.0 — the AAC release

23 commits since v0.7.0, all in three days. glint is no longer an MP3
encoder: it is a dual-codec (MP3 + AAC-LC), MIT-licensed, clean-room
encoder
whose AAC side places behind only Apple and Fraunhofer FDK
in quality at 128 kbps — while needing 47.4 KB of RAM and encoding
with no per-coefficient floating point on FPU-less microcontrollers.
Every claim below is measured; the full experiment log including every
dead end lives in PLAN.md (§A0–A6).

To our knowledge this makes glint the only competitive-quality AAC-LC
encoder under a plain OSI license: ffmpeg's native encoder is LGPL (and
behind on quality), FDK's license is not OSI-approved and disclaims
patent grants, and vo-aacenc (Apache-2.0) is unmaintained and places
last on every clip in our league.

The AAC-LC encoder

Written from the ISO 13818-7 / 14496-3 specs in three days of measured,
gated passes:

  • Wire format: all four window sequences (short blocks with
    attack-split grouping), per-band M/S stereo, selective TNS,
    optimal-sectioning Huffman (per-band codebook DP with exact bit
    accounting — the count==emission identity is unit-tested), CBR with a
    bit-debt controller, constant-quality VBR (V0–V9), ADTS output, all
    12 standard sample rates, mono/stereo.
  • Normative tables extracted from two independent implementations
    (vo-aacenc and ffmpeg), cross-checked bit-for-bit and verified
    prefix-free by the generator (tools/gen_aac_tables.py).
  • Psychoacoustics: metric-aligned Bark masks with tonality-aware
    offsets at low rates, and a distortion-controlled allocator
    per-band scalefactors in closed form from noise targets
    mask^0.6 · k, with the loudness knob bisected to the bit budget.
    Short frames get per-group masks and their own allocation tilt.
  • Validated by two independent decoders: every configuration
    decodes with zero errors in both ffmpeg and Apple CoreAudio, which
    produce metrically identical output. A decode-based ctest gate
    covers 8 configurations including a transient torture case and VBR.

Quality (measured league, 2026-07-06)

128 kbps stereo, mean noise-to-mask ratio in dB (lower is better;
rank ①–⑥ among the six encoders; -q normal):

clip Apple FDK glint LAME-MP3 ffmpeg vo-aacenc
speech −6.9 −5.4 −3.6 ⑶ −2.2 −0.7 +1.4
electronic −9.9 −11.4 −4.3 ⑶ −3.1 −1.9 +2.3
quartet −2.9 −2.6 −5.8 ① −2.0 +0.2 +1.2
industrial −1.1 −0.8 −1.4 ① −0.0 +1.6 +1.3
piano −9.4 −8.5 −10.2 ① −7.5 −4.1 −1.8
castanets −7.4 −9.0 −8.5 ⑵ +2.6 +7.1 +18.6

On castanets glint's PEAQ ODG is 0.00 — the best of all six encoders
(Apple and FDK: −0.08): the short blocks, TNS and per-group shaping land
the transients transparently. At 256 kbps everything from
Apple/FDK/glint is ODG-transparent; glint has the best NMR of all
encoders on quartet (−18.7 vs FDK's −13.3) and the highest SNR of the
AAC field on most clips. The full 6-clip × 2-rate log with
SNR/NMR/ODG/PESQ/STOI columns ships in tests/aac_league_v0.8.txt.
vo-aacenc — the obvious "just port it" candidate when this work started
— finishes last on essentially every cell.

Constant-quality VBR: V0 = 311 kbps / 0.0 % audible band-frames on
the speech clip (better than CBR-256), V4 ≈ 130–160 kbps
content-adaptive, V9 ≈ 42 kbps. No Xing-style header machinery needed —
ADTS frames are self-describing.

Speed

After a profile-driven, byte-identity-gated perf pass (−52…54 % encode
time): ~272× realtime at -q speed and ~168× at -q best on an
Apple M1 (integer build: 241× / 140×) — faster at -q speed than every
encoder in the league, including Apple's.

Embedded: 47.4 KB and no FPU

  • GLINT_MODE=fixed builds the AAC encoder in 47.4 KB (24.3 KB
    context + 23.1 KB tables) — under vo-aacenc's measured 48.0 KB — with
    storage-only type changes that are metrics-identical to the desktop
    build, then adds GLINT_AAC_INT: integer MDCT (131 dB transform
    SNR), a Q16 log-domain integer quantizer (0.04 coefficient
    mismatches per frame vs the double formula), and int64 energies
    so
    that at -q speed no per-coefficient float instruction remains.
  • MP3 got the same treatment (GLINT_MP3_INT): the old "fixed"
    path was Q31 only through the filterbank; now the -q speed CBR
    chain is integer end-to-end (new integer 36-pt MDCT + the shared
    log-domain quantizer), metrics-identical to the double rate loop
    within 0.02 dB. MP3 quality is otherwise bit-for-bit unchanged from
    v0.7.0 (regression gate deltas: +0.00 across the board).
  • Provable: tools/check_nofpu.sh disassembles a Cortex-M0+ build
    and fails if any per-coefficient function contains a soft-float
    call. It passes.
  • Runnable without hardware: embedded/qemu/ runs the benchmark
    on an emulated Cortex-M3; the semihosted output streams decode
    cleanly in ffmpeg and CoreAudio, and the MP3 stream is bit-exact
    with a host run
    (the integer path is deterministic across
    architectures). Ready-to-flash benchmark projects for the Raspberry
    Pi Pico (embedded/pico/) and ESP32 (esp-idf/example/) are
    included — QEMU is not cycle-accurate, so real-silicon throughput
    numbers are the one thing still open.

API, bindings, tooling

  • New C API: glint_aac_create/encode/encode_float/flush/destroy,
    glint_version(). The AAC config struct carries a reserved tail and
    a zero-init contract so future options never break the ABI.
    Encoder delay is 2048 samples; glint_aac_flush returns the two
    tail frames.
  • AAC in all three bindings (Python AacEncoder, Rust
    AacEncoder/encode_pcm_aac, Dart GlintAacEncoder) — and a fix
    for a latent bug in all of them: the mirrored MP3 config structs
    were missing fields the C struct has had since the quality/VBR
    features, so glint_create read uninitialized memory.
  • The encoder league harness gained --codec aac
    (tests/compare_encoders.py): glint vs Apple afconvert, fdkaac,
    ffmpeg native and vo-aacenc, with PEAQ ODG / PESQ / STOI columns.
  • A listening pack generator convention and a double-blind ABX tool
    (tests/abx.py) for ear-verification of any pair.

Compatibility notes

  • struct glint_aac_config must be zero-initialized (= {0} or
    memset) before filling — reserved fields select defaults.
  • The fixed-point (GLINT_MODE=fixed) AAC build trades ~0.7–1.5 dB
    NMR on stereo versus the float builds (the irreducible half-LSB of
    integer M/S, proven by an L==R experiment; mono is identical,
    castanets is better). Desktop builds are unaffected.
  • MP3 encoding is bit-for-bit identical to v0.7.0 in the double path
    and at -q normal/best in the fixed path; only fixed -q speed
    changed (integer quantizer, metrics-identical).

v0.7.0 — the quality release

Choose a tag to compare

@github-actions github-actions released this 04 Jul 19:29

glint v0.7.0 — the quality release

124 commits since v0.6.0. This release takes glint from "~15 dB SNR at every
bitrate" (a single quantizer-curve bug) to measurably ahead of LAME on 8 of
10 test clips at 128 kbps
(PEAQ ODG) and ahead on SNR across the board at
256 kbps — while cutting embedded RAM to ~64 KB, one third less than
Shine
. Every claim below is measured; the full experiment log, including
every dead end, lives in PLAN.md.

Headline numbers (vs LAME 3.100 -q 2, identical inputs)

128 kbps joint stereo, PEAQ ODG (closer to 0 is better):

clip glint LAME clip glint LAME
choir −1.28 −2.06 piano −0.63 −0.70
orchestral −0.75 −1.09 torture −0.32 −0.45
quartet −0.46 −0.88 speech −1.24 −1.29
industrial −0.82 −1.00 electronic −0.25 −0.22
drums −1.18 −1.03 castanets −0.62 −0.29

256 kbps joint: everything ODG-transparent; glint ahead on SNR throughout
(speech 38.4 vs 36.9 dB, electronic 44.8 / NMR −18.0 vs 44.5 / −15.8, quartet
44.9 vs 46.0 with NMR −13.7 vs −11.2). Castanets mean noise-to-mask is
glint's at both rates. 64 kbps stereo: glint ahead on ODG (−3.17 vs −3.32).
MPEG-2 22.05 kHz @ 64k: 21.6 dB vs LAME's 17.6.

Correctness fixes (wire format & math)

  • pow34 quantizer curve: the old integer-grid table degenerated to
    identity on (0,1) — quantizer inputs are fractional — capping the whole
    encoder at ~15 dB SNR independent of bitrate. Root cause of everything.
  • sfb21 out-of-bounds scalefactor read: a phantom ~29× HF boost with no
    decoder-side counterpart; fixing it was worth +8..17 dB SNR on music.
  • MPEG-2 scalefac_compress: glint used an invented mapping — internally
    consistent, garbage for every real decoder. Now ISO 13818-3.
  • Short-block scalefactor band tables: the 44.1 kHz table had an
    off-by-two boundary (138 vs ISO's 136), and the MPEG-2 tables were MPEG-1
    copies that scrambled the wire order — the real cause of the historical
    LSF short-block collapse.
  • preemphasis[20]: 3 → 2 per ISO (desynced band 20 whenever preflag
    was set).
  • LSF start/stop region boundary: decoders hardwire region0 = 54 lines
    (not 36) for LSF transition granules.
  • VBR budget bugs: quantized under the caller's default frame budget
    instead of the max; assumed the CBR padding byte and overflowed the
    largest unpadded frame.
  • Small-buffer frame overflow: a 320 kbps frame silently overflowed the
    1024-byte embedded frame buffer; buffers are now sized for every legal
    frame and glint_create rejects what cannot fit.

New encoder machinery

  • Bit reservoir + rate control (CBR): continuous main-data stream with
    deferred emission, buffer-feedback constant-quality anchor, post-transient
    banking (joint mode). Worth +1..2.6 dB at all rates.
  • Short blocks: start/stop transition windows, per-frame scheduler with
    one-granule lookahead, per-window subblock_gain, full per-(band,window)
    short scalefactors, 6× attack threshold, 2-granule attack-decay extension.
    Live for MPEG-1 and MPEG-2/LSF, on both signal paths (fixed-point
    now matches the double path to ≤0.06 dB on transients).
  • Psychoacoustic allocation: Schroeder-Bark masking aligned with the
    measurement metric drives scalefactor noise-shaping loops for long and
    short granules, shaping ~9 dB below the mask under a total-noise guard,
    with tonality-adaptive masker offsets at ≤96 kbps/channel and
    scalefac_scale escalation. Runs in the VBR path too.
  • Bitrate-scaled encoder lowpass with content-aware sfb21 keep at high
    rates: the no-scalefactor sfb21 region is zeroed where it would only
    collect quantizer spray, kept where it holds real content.
  • Optimal Huffman region splits on finished granules (exhaustive
    region0/region1 search over per-table prefix costs); real per-region table
    selection by actual bit count; preflag folding.
  • VBR: real variable-size frames, psy shaping, Xing header with seek
    TOC and gapless LAME tag
    — players show correct duration, seek, and trim
    the codec delay sample-exactly (ffmpeg decodes at a 0-sample offset). New
    API: glint_vbr_header().

Embedded

  • GLINT_MODE=fixed RAM: ~64 KB (Shine: ~96 KB), measured as static/BSS
    • encoder context. Achieved via a mantissa cube-root table replacing a
      64 KB LUT (metrics-identical), single-slot per-samplerate model caches,
      float transition tables, and dead-code removal.
  • Fixed path now has short blocks, transition windows, and the encoder
    lookahead — embedded output quality equals desktop.

Performance

Three profile-driven optimization passes (byte-identical where claimed):
Huffman pair-cost LUTs, fused region/table selection, gain-search state
reuse, MDCT table fusion, -O3/LTO, and an opt-in threaded scale-factor
search (-j N, byte-identical for any thread count). Encoder speed ~260× /
52× / 34× realtime (speed/normal/best, Apple M1, 256 kbps stereo).

Testing & tooling

  • GitHub Actions CI: build matrix (double/fixed/both × Linux/macOS),
    unit tests, decode-based tests, full quality suite.
  • Decode-based MPEG-2 regression test in ctest — the class of wire bug
    that unit tests provably missed.
  • Encoder league harness (tests/compare_encoders.py): glint vs LAME vs
    Shine over a 10-clip lossless corpus with SNR, Bark-band NMR, PEAQ ODG,
    ViSQOL, PESQ and STOI, plus a regression gate (--check) against recorded
    baselines.
  • 10-clip corpus: speech, four music genres from lossless sources
    (incl. 24-bit/96 kHz CC0 piano and CC-BY choir), synthetic castanets and a
    deterministic torture clip (generators in tests/).
  • ABX listening tool (tests/abx.py): aligned, loudness-normalized,
    double-blind, binomial p-value.

Behavior changes

  • Encoder latency is now 1104 samples (filterbank chain + one-granule
    lookahead) on both paths; VBR streams start with a placeholder frame that
    glint_vbr_header() finalizes (unrewritten streams decode with ~26 ms of
    leading silence).
  • Content above the rate-dependent lowpass (15.8 kHz at ≥128 kbps/ch,
    44.1 kHz) is dropped unless the content-aware keep retains it.
  • 320 kbps at 32 kHz and similar corner configs are rejected by
    glint_create in small-buffer builds only if they cannot fit (none of the
    legal ones are rejected anymore).

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 24 Jun 08:19

Full Changelog: v0.5.0...v0.6.0

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 24 Jun 07:31

Full Changelog: v0.4.1...v0.5.0

v0.4.1

Choose a tag to compare

@github-actions github-actions released this 23 Jun 21:06

Full Changelog: v0.4.0...v0.4.1

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 23 Jun 21:02

Full Changelog: v0.3.0...v0.4.0

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 23 Jun 19:56

Full Changelog: v0.2.0...v0.3.0

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 23 Jun 19:13

Full Changelog: v0.1.0...v0.2.0

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 23 Jun 18:46