Skip to content

Feasibility: MINIMP3_FIXED_POINT — CC0 fixed-point minimp3 mode, golden-tested vs float, to replace RPSL libhelix_mp3 #13

Description

@zackees

Note

AI feasibility study (Claude Fable agent, 2026-08-25) including a compiled numerical prototype run on Windows/MinGW gcc 12.2.0. Resolves the libhelix_mp3 open question from #10: a straight minimp3 swap was ruled out (float core, not realtime on MCU), but a fixed-point mode for minimp3, validated by golden tests against its own float build, using Helix's architecture as a clean-room design reference is feasible. Prototype source: fixproto.c (16.5KB, preserved in the session workspace; can be committed on request).

Feasibility: MINIMP3_FIXED_POINT — a CC0 fixed-point mode for minimp3, golden-tested against float, replacing RPSL libhelix_mp3

Verdict

Feasible-with-caveats — and worth doing. ~6 person-weeks (+2 contingency). No fixed-point minimp3 fork exists anywhere; this fills an empty niche and is upstreamable to lieff/minimp3 as CC0.

Grounding: the two pipelines are stage-isomorphic (dequant → stereo → reorder → antialias → IMDCT → DCT32 → polyphase); a working prototype of the two transform stages in fixed point, with every constant regenerated from closed-form ISO formulas (matching minimp3's literals to 5.9e-8), achieved:

DCT32  (6 guard bits, full-scale random): PSNR = 140.2 dB, max abs err = 41.7 LSB (int32 @ Q25)
DCT32  (low-level input, 13 gb):          PSNR =  98.0 dB
IMDCT36 chain (7 gb, 20k blocks, overlap carried): PSNR = 138.5 dB, max err = 11.8 LSB

Per-stage fixed-point noise sits ~25–28 dB below the 16-bit PCM quantization floor. Estimated MAC count lands within ~20% of Helix (~55–75k multiplies/frame), whose realtime record on exactly FastLED's targets (ESP32 LX6/LX7, ESP32-C3 RISC-V, RP2040, Cortex-M3/M4) is proven. Memory is a wash: ~23KB working RAM vs Helix's ~24KB — with the mandatory fix of moving minimp3's 16.3KB mp3dec_scratch_t off the stack (needed for MCUs regardless of float/fixed).

Helix fixed-point architecture (what we learn from it — as ideas only; it is RPSL)

  • One primitive everywhere: MULSHIFT32(x,y) = top 32 bits of signed 32×32→64 (real/assembly.h) with per-ISA implementations (ARM smull, Xtensa mulsh, RISC-V mulh, generic C). 64-bit MADD64 accumulation confined to the polyphase filter only.
  • Q-formats per stage: dequant out Q25 with implicit 2^15 bias; stereo tables Q30; antialias/IMDCT coefficients Q31/Q30; DCT32 per-constant Q31→Q27 with hardcoded restore shifts; polyphase coefficients Q30 pre-shifted 12.
  • Guard-bit machinery is the architectural heart: every stage ORs outputs into a mask; gb = CLZ(mask)−1 flows downstream; IMDCT36 demands 7 gb and DCT32 6 gb, with a rare dynamic pre-shift + rescale-with-clip when the budget is short (triggered "5–10% of the time" for IMDCT, "almost never" for DCT32). Floats made this unnecessary in minimp3 — it is the one piece of architecture that must be grafted on.
  • pow(x,4/3) dequant: magnitude-split LUTs + quarter-power gain folding (2 low bits of gain → 2^(-k/4) fraction table, rest → shift).
  • Memory: ~24KB heap across 7 mallocs, tiny stack (design point: ARM7TDMI/Cortex-M3-class).

minimp3 float pipeline map (verified against minimp3.h source)

Scalefactors → floats via L3_ldexp_q2 (4-entry 2^(-i/4) table × shift — already structured for a mantissa/exponent split); Huffman fuses dequant via one float multiplier per scalefactor band (*dst = one * L3_pow_43(lsb) * sign); pow43 uses a 145-entry LUT + a CC0 second-order frac-interpolation formula for large values (minimp3.h:737-739) — no minimax polynomial needed; mid-side is plain add/sub; antialias g_aa < 1.0; IMDCT36 = 9-point DCT-III pairs + twiddles + window (all constants < 1); DCT32 g_sec has multipliers up to 10.19 (→ per-constant Q27 with restore shifts — prototype-validated); polyphase g_win values are already exactly integer-valued floats (ISO D-window ×2^16) — the table transplants to int32 unchanged; output clamp mp3d_scale_pcm is where MINIMP3_FIXED_POINT naturally branches.

Persistent state 6.7KB; per-call scratch 16.3KB on the stack (minimp3.h:232-239) — must move to caller-provided scratch (mp3dec_decode_frame_r(dec, scratch, …), old API preserved on hosts).

Hardest stage: the fused Huffman/dequant — scf must become (Q31 mantissa, int8 exponent) pairs, pow43 needs integer paths, and it decides dynamic range for everything downstream.

Conversion design (per stage)

#define MINIMP3_FIXED_POINTtypedef int32_t mp3d_real_t + integer kernels; float path (incl. all SIMD) compiles unchanged otherwise. Q25-with-bias data convention; guard-bit OR-mask/CLZ tracking end-to-end; per-constant Q-format with restore shifts for >1.0 coefficients; 64-bit MACs only in polyphase; all tables regenerated from ISO 11172-3 formulas (prototype: 6e-8 agreement with minimp3's own literals); large-value pow43 = fixed-point version of minimp3's own CC0 formula (avoids needing any Helix polynomial). L1/L2 gated behind MINIMP3_ONLY_MP3 for v1.

Golden-test plan (the validation the steward asked for)

  • Corpus: ISO/IEC 11172-4 + 13818-4 compliance vectors; LAME-encoded synthetics — 997Hz sine at −0dBFS and −60dBFS (the low-level case constantly exercises the guard-bit path that full-scale tests hide), sweeps, impulses (window switching), noise at low bitrate (linbits); forced mid-side, MPEG-1 and MPEG-2/2.5 intensity stereo; low sample rates; free-format; VBR with reservoir stress; truncated/garbage streams for resync.
  • Harness: both modes in one binary (separate TUs with distinct prefixes). Frame alignment must be identical before metrics. Metrics per file and per 10ms window: max-abs-diff, RMS, PSNR.
  • Gates: ISO "limited accuracy" as floor, full accuracy targeted; PSNR ≥ 90 dB and max-abs-diff ≤ 8 LSB vs the float golden on every file (prototype indicates ≥96 dB is realistic — ratchet up); −60dB content gets a signal-relative gate (≥55 dB) to catch guard-bit bugs.
  • Per-stage golden hooks (MINIMP3_STAGE_DUMP): both builds dump per-granule intermediates after each of 6 stages; comparator names the first granule/stage/coefficient over epsilon — regressions localize to a stage instead of "output differs." ~200 lines; highest-value piece of the plan.
  • Differential fuzzing: both modes under ASan/UBSan must agree on accept/reject and frame_bytes, never crash, and agree on PCM within threshold when both accept.
  • CI: host matrix + nightly fuzz + cross-builds for Xtensa/riscv32-esp-elf/arm-none-eabi (M0+/M4) with -Wframe-larger-than and scratch-size asserts; on-target cycles/frame benchmark tracked, advisory in PR, blocking at release.

Performance & memory projection

~55–75k multiplies/frame ≈ 2.1–2.9M mult/s at 38.3 frames/s — within ±20% of Helix. Realtime 44.1kHz stereo projected with large margin on ESP32 LX6/LX7 (has mulsh), ESP32-C3 (has mulh), Cortex-M3/M4; RP2040 M0+ is the tight one (64-bit MACs ≈ 5–6 instructions; projected 40–60% of one core, comparable to Helix; benchmark in week 2, with a measured fallback of 32-bit truncated MACs in the window at ~1-bit SNR cost). Memory ≈ 23KB working RAM vs Helix ≈ 24KB.

Clean-room & licensing

  • Take from Helix (unprotectable ideas/methods): Q25+bias convention, guard-bit accounting concept, per-constant Q-format encoding, 64-bit-MAC-only-in-synthesis, magnitude-split pow43 LUT concept.
  • Never take: Helix code text, table literals, comments, or its minimax coefficients (poly43lo/hi) — regenerate anything needed, or use minimp3's own CC0 large-x formula (chosen design does the latter).
  • Process hygiene: the implementer works from this design doc + minimp3 + ISO math with Helix source closed; table-generator scripts committed in-repo.
  • Result: cleanly CC0; propose upstream to lieff/minimp3 as MINIMP3_FIXED_POINT; if upstream is dormant, carry in-tree as CC0 (not FRL) for maximum portability. Footnote: lieff/minimp3's ancestry (keyj → FFmpeg-derived) is the ecosystem-accepted baseline risk, unchanged by this work, and strictly better than shipping RPSL.

Prior art

No fixed-point fork of lieff/minimp3 exists (searched; only float forks: kcat/, pschatzmann/arduino-minimp3, tosone/). All integer relatives are license-encumbered: FFmpeg fixed decoder (LGPL), libmad (GPL), Helix (RPSL). The embedded community's documented answer to "minimp3 too slow on MCU" is literally "use Helix" (cmorgan.org 2023 — who concludes a fixed-point minimp3 conversion would be required; pschatzmann.ch 2022 ESP32 measurements). A CC0 fixed-point minimp3 fills a genuinely empty niche — which also argues for upstreaming.

Staged plan (~6 person-weeks + 2 contingency)

Stages land behind the golden hooks so float and fixed stages interoperate during bring-up (Q25↔float is one ldexp at stage boundaries):

  1. Synthesis back-end (DCT32 + polyphase + scratch-off-stack API) — prototype-validated, biggest cycle win, de-risks 64-bit MAC codegen on all four ISAs. 1 wk (+1 wk harness first).
  2. IMDCT36/12 + antialias — prototype-validated core; first guard-bit pre-shift path. 1 wk.
  3. Huffman/dequant + scalefactors — the hard one (mantissa/exp scf, integer pow43, mask/CLZ plumbing). 1.5 wk.
  4. Stereo + end-to-end gb tracking. 1 wk incl. fuzz/target validation.
  5. L1/L2 (or keep MINIMP3_ONLY_MP3), packaging, upstream PR. 0.5 wk.

Top 3 risks: (1) dynamic-range corners (max global_gain × low-level content vs gb budget) → −60dB gates + stage hooks + rare-clip concept; (2) integer pow43 linbits accuracy/cost → validate standalone against powl over the full domain before integration; (3) M0+ cycle budget → benchmark stage 1 on RP2040 in week 2, fallback measured.

Acceptance gate for deleting libhelix_mp3: (a) all golden tests green (ISO floor, ≥90 dB vs float golden across the full corpus, stage hooks clean); (b) 72h differential fuzz, zero divergence/crash; (c) on-target: 44.1kHz stereo 320kbps at ≤60% of one core on ESP32-C3@160MHz and RP2040@133MHz, ≤25% on ESP32@240MHz, scratch off-stack; (d) A/B listening sanity. When those hold: RPSL tree deleted, fixed-point minimp3 PR'd upstream.


Sources: lieff/minimp3 (github.com/lieff/minimp3), minimp3.h source, cmorgan.org/2023/10/05 (MP3 Decoding on Embedded — by the maintainer of the exact libhelix fork FastLED vendors), pschatzmann.ch 2022-05-14 (minimp3 on ESP32), keyj.emphy.de/minimp3 (origins). Helix architecture findings cite the vendored files under src/third_party/libhelix_mp3/ (real/assembly.h, real/dqchan.cpp.hpp, real/imdct.cpp.hpp, real/dct32.cpp.hpp, real/polyphase.cpp.hpp, real/trigtabs.cpp.hpp, real/buffers.hpp).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions