jd.Codec · MP3 encoder

An in-house MP3 encoder. Measured.

iOS ships without an MP3 encoder — jd.Edit brings its own. jd.Codec is a complete in-house development in Swift, without a single line of third-party code: psychoacoustic model, noise shaping, joint stereo, bit reservoir, short blocks, sample-accurate gapless. Calibrated against the reference encoder LAME — and consistently ahead of it in the lab. The MP3 patents expired worldwide in 2017; the format is free to implement.

19 / 20
measurements ahead of LAME
+7.3 dB
average SNR lead across 18 overall scores
116 / 116
round-trip checks against Apple's decoder passed
90–150×
real-time speed on Apple Silicon

The complete method

The signal path of every frame — 1152 samples, two granules — passes through six stages. Every one of them implemented from scratch:

PCM Float ±1Polyphase filter bank512-tap window · 32 subbandsTransient detectorhigh-pass energy · 2-granule lookaheadBlock sequence: long → start → short → stoplong/start/stopshortMDCT 36→18 per subband+ alias reduction at 31 band edges3 × MDCT 12→6 per subbandstored in bitstream order576 spectral values xrstereo? M/S pays offPsychoacousticsmasking threshold per band + perceptual entropyM/S joint stereoM=(L+R)/√2 · thresholds capped at min(L,R)Bit-rate & budget allocationCBR: frame + reservoir · VBR/ABR on demandNoise-shaping loopbinary global_gain search · minimax scale factorsHuffman codingbig_values · count1 · rzero — table choice per regionBitstreamframe shells + main data stream · main_data_beginXing/LAME info frameframe count · seek TOC · delay/padding — gapless

The signal path of one frame — 1,152 samples per channel, two granules of 576.

1
Analysis filter bank (hybrid)
Per granule, 18 blocks of 32 samples pass through the standard's polyphase filter bank: 512-sample history buffer, ISO analysis window (table C.1), 32×64 cosine matrix. Per subband the MDCT follows over 18 old + 18 new time steps with the block type's window shape and alias reduction at the 31 band edges (table B.9); short blocks split the 36 time steps into three overlapping 12-sample windows of 6 spectral lines each, stored directly in bitstream order. The MDCT normalization — factor 9 long, 3 short (N/4) — was not derived but measured against Apple's decoder.
2
Psychoacoustics
Per scale-factor band (21 long or 12×3 short): band energies straight from the MDCT spectrum, tonality at the masker via spectral flatness (offset O = tonality·(14.5 + Bark) + (1−tonality)·5.5 dB), Schroeder Bark spreading — row-normalized. Plus the minval cap (24.5 − 1.6·Bark dB minimum SNR), masking_lower tightening thresholds toward the bass by up to 8 dB, the threshold in quiet (Terhardt) as the floor, and +1.2 dB per Bark of relaxation above ~5 kHz. Windows before an attack get thresholds 10 dB stricter — pre-echo control. By-product: the perceptual entropy Σ width·log₂(E/xmin)/2 as the bit-demand measure.
3
Bit allocation
CBR: capacity is always distributed, weighted by entropy — otherwise bits expire, since the reservoir holds only 511 bytes. The reservoir funds peaks on top, short granules (attacks) get a bonus, and a budget hysteresis smooths stationary passages against granule-rate pumping. VBR: per frame, the smallest table rate whose capacity covers the scaled demand (quality 0–9 ≙ factor 1.6…0.5). ABR: a controller around the target rate — the target on average, demanding frames above it, a deficit term keeping the mean on course.
4
Quantization with noise shaping
Inside: a binary search over the step size (global_gain) until the Huffman coding fits the budget — ix = (|xr|·2^(−s/4))^¾ with the standard's −0.0946 rounding offset, x^¾ via two square roots instead of pow. Outside: bands above threshold get boosted via scale factors. Two details decide: minimax selection (keep the attempt with the smallest worst band ratio — the plain sum is invariant under redistribution) and 3 dB selectivity (only bands near the biggest outlier). Short blocks additionally: subblock_gain in 12 dB steps per window.
5
Huffman coding and bitstream
Partitioning from the back: rzero zero pairs, count1 quadruples ≤1 (table choice A/B), the remaining big_values in up to three regions along the scale-factor bands — table choice per region including the ESC families 16/24 with linbits. The bitstream consists of frame shells over a continuous main data stream: main_data_begin points into the byte-accurate bit reservoir (511-byte cap, flushed at stream end). Conservatively over-counted bits are stuffed with one-bits — they decode as empty count1 quadruples beyond index 576.
6
Gapless (Xing/LAME tag)
MP3 has no exact lengths: the encoder delay up front (528 samples, measured), the last frame's padding at the end. The file's first frame is therefore a silent info frame: a Xing block with frame count, byte count and seek TOC — built from real frame sizes under VBR — plus a 36-byte LAME tag with delay, end padding and tag CRC. finish() flushes both filter banks (528 + 529 zeros); every decoder's cutting rule is delay + 529 at the front, padding − 529 at the end. Result, verified against three decoders: exactly the fed-in sample count, signal start at sample 0.
Featurejd.CodecLAME
Psychoacoustic model (Bark spreading, tonality, ATH)
Noise shaping via scale factors (minimax outer loop)
Adaptive bit allocation by perceptual entropy
M/S joint stereo with binaural threshold cap
Bit reservoir (main_data_begin, up to 511 bytes)
Short blocks with transient detector
VBR (quality 0–9) / ABR with target-rate control
Sample-accurate gapless (Xing/LAME tag, verified against 3 decoders)
Decades of blind-listening tuning

Scope: CBR 128–320, VBR (quality 0–9) and ABR 32–320 kbit/s · 32/44.1/48 kHz · mono and stereo · sample-accurate gapless — in roughly 2,400 lines of pure Swift.

How it was tested

The testing strategy was the foundation from day one: 116 round-trip checks encode synthetic signals with a known reference, decode with Apple's decoder — the same one that will play the files later — and measure SNR after exact alignment via cross-correlation and least squares. Wherever the specification stayed ambiguous, the decoder became the oracle:

1
Forced single line
A hand-placed spectral line written straight into the bitstream — cleanly separates bitstream bugs from analysis bugs.
2
Convention matrix
Every contested sign and ordering variant of the filter bank permuted as switches, SNR measured — instead of spec guesswork.
3
Matrix identification
Decoded channels regressed against L and R via least squares — reveals exactly which stereo matrix a decoder really applies.
4
Three decoders as jury
Apple, LAME and ffmpeg as cross-check: if all three agree, the bug is the encoder's. If they differ, it's a decoder quirk.
Measurement traps — the measurement itself can lie
The measurement itself can lie: periodic test signals let the delay search lock onto side peaks — hence inharmonic prime-number partials. A probe window in the quiet section read 9 dB instead of 77 — hence it is tied to the signal structure. And debug samples from the fade-in look transparent while the steady part struggles — checks start at granule 60.

The complete measurement setup is script-driven — test harness and benchmark run without manual intervention, and every number on this page comes reproducibly from the same run (LAME 3.100, -q 0, decoder: Apple AVAudioFile). As of August 7, 2026.

The origin story — with every challenge

Built in August 2026, chronologically in eight acts — the wrong turns are the most instructive part, so they are included:

1
Deciding against the dependency
iOS cannot write MP3, LAME would be an LGPL dependency — so write it ourselves. The MP3 patents expired in 2017; the embedded number tables are ISO specification data, every Huffman table verified with a Kraft sum of exactly 1.0.
2
The factor of 9
The first bitstream was valid right away — and sounded distorted: SNR constant at 9 dB. A forced single line exposed the missing MDCT normalization. One factor of 1/9, and 9 dB became 78. Lesson: measure the order of magnitude first, then hunt signs.
3
The loop that would not tip
Four attempts to real noise shaping: water filling discarded, the sum score swapped for minimax, selectivity sharpened to 3 dB, two hidden threshold floors removed. Since then the loop demonstrably shapes — low-frequency noise −14 dB.
4
Three decoders tell the same truth
M/S first destroyed the right channel — S errors are not masked binaurally, the thresholds need a min(L,R) cap. Then every decoder ignored the M/S bit: mode_extension only counts with mode = 01. A two-bit fix — and stereo beat LAME.
5
Bits expire if you don't spend them
The reservoir mechanics worked immediately, the budget policy took two attempts: always distribute capacity, use the reservoir only for peaks. The dynamics scenario then jumped from 56.6 to 76.3 dB.
6
The window chain is a contract
The complete short-block path passed the round trip on the first try — the reward of the oracle method. The stumbles were next to it: attacks are detected on the high-pass, and the lookahead must use exactly the same formula as the decision itself, two granules ahead.
7
229 lost samples
finish() flushed only one of two filter banks — the last samples of every recording stayed stuck in the decoder. Plus two conventions determined by experiment: Apple accepts the info frame only with the magic word “Xing”, and all decoders cut delay + 529 at the front, padding − 529 at the end. Final state: sample-accurate across three decoders.
8
The entropy was already there
VBR nearly fell out by itself — perceptual entropy is the demand signal, and the reservoir already handled variable frames. Only the ABR controller had to learn to modulate around the target rate instead of scaling the demand.

Results

Four mono scenarios, three bitrates — higher is better.

128 kbit/s

jd.CodecLAME (q0)Two-tonejd.Codec: 80.4 dB80.4LAME: 58.2 dB58.2Multi-tonejd.Codec: 70.7 dB70.7LAME: 63.6 dB63.6Tone in noisejd.Codec: 27.8 dB27.8LAME: 22.1 dB22.1Dynamicsjd.Codec: 70.1 dB70.1LAME: 55.7 dB55.7

192 kbit/s

jd.CodecLAME (q0)Two-tonejd.Codec: 80.4 dB80.4LAME: 73.0 dB73.0Multi-tonejd.Codec: 77.4 dB77.4LAME: 76.1 dB76.1Tone in noisejd.Codec: 34.7 dB34.7LAME: 25.2 dB25.2Dynamicsjd.Codec: 76.2 dB76.2LAME: 74.7 dB74.7

256 kbit/s

jd.CodecLAME (q0)Two-tonejd.Codec: 80.4 dB80.4LAME: 75.7 dB75.7Multi-tonejd.Codec: 77.6 dB77.6LAME: 76.1 dB76.1Tone in noisejd.Codec: 42.1 dB42.1LAME: 25.9 dB25.9Dynamicsjd.Codec: 76.6 dB76.6LAME: 69.0 dB69.0

Overall SNR in dB, identical input, decoded with Apple's decoder.

The lead at a glance

Difference jd.Codec − LAME across all 20 measurements, sorted. All 18 overall scores go to jd.Codec; the one negative bar is a sub-metric far below the audibility threshold.

jd.Codec aheadLAME aheadTwo-tone · 128+22.2 dB+22.2Tone in noise · 256+16.2 dB+16.2Click bursts · 128+15.6 dB+15.6Dynamics · 128+14.4 dB+14.4Tone in noise · 192+9.5 dB+9.5Dynamics · 256+7.6 dB+7.6Two-tone · 192+7.4 dB+7.4Stereo left · 128+7.2 dB+7.2Multi-tone · 128+7.1 dB+7.1Tone in noise · 128+5.7 dB+5.7Pre-echo residual · 128+5.5 dB+5.5Stereo right · 128+4.9 dB+4.9Two-tone · 256+4.7 dB+4.7Stereo left · 192+2.4 dB+2.4Click bursts · 192+2.2 dB+2.2Dynamics · 192+1.5 dB+1.5Multi-tone · 256+1.5 dB+1.5Multi-tone · 192+1.3 dB+1.3Stereo right · 192+0.6 dB+0.6Pre-echo residual · 192−7.2 dB−7.2

Band detail at 128 kbit/s

The hardest case, resolved by frequency band (Hz). Cell format: jd.Codec / LAME and difference in dB.

Signaloverall0–3000.3–1k1–3k3–8k8–16k
Two-tone80.4 / 58.2 +22.283.2 / 59.0 +24.23.1 / −14.4 +17.5−5.9 / −3.2 −2.748.7 / 27.4 +21.3−6.8 / −3.9 −2.9
Multi-tone70.7 / 63.6 +7.174.6 / 67.6 +7.069.4 / 62.1 +7.363.4 / 56.7 +6.758.5 / 51.5 +7.050.4 / 43.7 +6.7
Tone in noise27.8 / 22.1 +5.714.8 / 29.2 −14.438.8 / 31.1 +7.712.4 / 18.6 −6.212.2 / 13.9 −1.712.2 / 10.0 +2.2
Dynamics70.1 / 55.7 +14.475.2 / 59.7 +15.570.2 / 55.5 +14.762.7 / 48.6 +14.159.6 / 45.3 +14.3−3.5 / −0.7 −2.8

How to read the noise-floor case: LAME spreads its noise more evenly across the mids, jd.Codec concentrates protection on the masking-critical bands (tone band 0.3–1k: +7.7 dB) and wins the overall score. Same bit budget, different allocation philosophy.

Stereo & transients

Measurementjd.CodecLAMEΔ
Correlated stereo left · 12856.6 dB49.4 dB+7.2
Correlated stereo right · 12852.8 dB47.9 dB+4.9
Click bursts, SNR · 12862.3 dB46.7 dB+15.6
Click bursts, SNR · 19269.7 dB67.5 dB+2.2
Pre-echo residual · 128 (lower = better)−60.5 dB−55.0 dB+5.5
Pre-echo residual · 192 (lower = better)−68.2 dB−75.4 dB−7.2

VBR spot check (dynamics signal, quality 2 vs. lame -V 2): 56.4 dB at 72 kbit/s versus 51.5 dB at 65 kbit/s — +4.9 dB at comparable file size. Gapless stays sample-accurate under VBR, too.

Honest limits

jd.Codec ships inside jd.Edit

The encoder is part of the jd.Edit waveform editor for iPhone — MP3 export with CBR, VBR and ABR is a Pro feature.

See jd.Edit