Releases: CrispStrobe/glint
Release list
glint v0.9.0 — the three-codec release
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
normativeopus_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.opusread 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_clinow 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, Q15tell_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 truncatedfmtchunk
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), andboth(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
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=fixedbuilds 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 addsGLINT_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 speedno 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 speedCBR
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.shdisassembles 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_flushreturns the two
tail frames. - AAC in all three bindings (Python
AacEncoder, Rust
AacEncoder/encode_pcm_aac, DartGlintAacEncoder) — 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, soglint_createread 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_configmust 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/bestin the fixed path; only fixed-q speed
changed (integer quantizer, metrics-identical).
v0.7.0 — the quality release
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 andglint_createrejects 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-windowsubblock_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=fixedRAM: ~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.
- encoder context. Achieved via a mantissa cube-root table replacing a
- 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 intests/). - 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_createin small-buffer builds only if they cannot fit (none of the
legal ones are rejected anymore).
v0.6.0
v0.5.0
v0.4.1
v0.4.0
v0.3.0
v0.2.0
v0.1.0
Full Changelog: https://github.com/CrispStrobe/glint/commits/v0.1.0