Alexandre Défossez, Jade Copet, Gabriel Synnaeve, Yossi Adi — Meta AI (FAIR), October 2022 · arXiv:2210.13438

EnCodec: High Fidelity Neural Audio Compression

Music at three kilobits per second that people still rate as good. The trick is not a cleverer transform — it is letting a network learn the transform, then discretising its output with a stack of codebooks that each clean up the previous one’s mistakes.

Prerequisites: what a spectrogram is (time × frequency picture of sound) + what a convolution does. Vector quantization, adversarial losses, and entropy coding are all built from zero here.
10
Chapters
14
Interactive Sims
1.5–24
kbps Range
256×
Compression at 6 kbps

Chapter 0: The Bandwidth Problem

Start with a number that sets the stakes. In 2021, streaming audio and video accounted for 82% of all internet traffic — that is the Cisco figure the paper opens with, and it is the entire economic reason this research exists. Every percent you shave off the bitrate of a voice call, a podcast, or a music stream is multiplied by billions of hours per day.

Now put a number on the raw material. A single second of CD-quality-ish mono audio at 24 kHz with 16-bit samples is:

24,000 samples/s × 16 bits/sample = 384,000 bits/s = 384 kbps

EnCodec compresses that to 6 kbps and human listeners still rate the result at 83.1 out of 100 for clean speech and 92.9 for music. That is a 64× reduction on the mono 24 kHz signal. For 48 kHz stereo music the reference is 48,000 × 16 × 2 = 1,536 kbps, and EnCodec at 6 kbps is a 256× reduction that ties MP3 running at 64 kbps.

Let that comparison land, because it is the headline of the whole paper. MP3 at 64 kbps scores 82.7 on the listening test. EnCodec at 6 kbps scores 82.9. Same perceived quality, one tenth the bits.

The frame for this whole lesson. Lossy compression is a two-objective problem: minimise the bitrate of a sample while also minimising the distortion according to some metric — ideally a metric correlated with human perception. Every design decision in EnCodec is a move in that two-dimensional space. When you meet a strange choice later (a discriminator that only sees spectrograms, a balancer that rescales gradients, a Transformer bolted onto the output), ask: which of the two axes is this buying, and what is it paying?

Where the bits actually go

Before any neural network, you need the arithmetic that governs every codec ever built. A codec produces a stream of symbols at some rate. Its bitrate is:

bitrate = (frames per second) × (bits per frame)

That is the whole budget. Both factors are design choices, and they trade against each other. EnCodec fixes the first factor by architecture and spends all its cleverness on the second.

Here is the specific number you should memorise now, because it recurs in every chapter. EnCodec’s encoder downsamples 24 kHz audio by a total factor of 320. So:

24,000 samples/s ÷ 320 = 75 latent frames per second

Every 320 input samples — 13.3 milliseconds of sound — collapse into one latent vector. That is the atom of the system. Everything downstream is a question of how many bits you are willing to spend describing that atom.

Divide the target bitrate by 75 and you get the per-frame budget directly:

Target bitrateBits per frame (÷ 75)What that buys
1.5 kbps1500 / 75 = 20 bits2 codebooks of 1024 entries (10 bits each)
3 kbps3000 / 75 = 40 bits4 codebooks
6 kbps6000 / 75 = 80 bits8 codebooks
12 kbps12000 / 75 = 160 bits16 codebooks
24 kbps24000 / 75 = 320 bits32 codebooks (the paper’s maximum at 24 kHz)

Twenty bits. At 1.5 kbps, the entire description of 13.3 ms of music — timbre, pitch, transient, reverberation — must fit in twenty binary digits. That is one number between 0 and 1,048,575. If that sounds impossible, it is because it is impossible in the classical framing, and Chapter 1 explains exactly why the classical framing runs out of room here.

Why the numbers are so clean. 320 is not an accident: it factors as 2 × 4 × 5 × 8, and those four numbers are literally the strides of the encoder’s four downsampling convolutions (Chapter 2). The paper chose strides whose product divides 24,000 evenly, so the frame rate is an integer and the codec never has to deal with fractional-frame bookkeeping. Good systems papers hide arithmetic conveniences like this in a single parenthetical; noticing them is how you learn to design your own.
Sim 1 — The bit budget: where 13.3 milliseconds of sound goes

Drag the bitrate slider across EnCodec’s five supported rates. The top bar is one second of audio split into 75 frames; the panel below opens one frame and shows the bits inside it as 10-bit codebook blocks. Watch the raw-audio reference bar (384 kbps) shrink out of view — the compression ratio is printed live.

Bitrate 6 kbps

What the reader should feel right now: doubt

Twenty bits per 13.3 ms is a shocking budget, so it is worth checking against a codec you already trust. Opus is the IETF’s general-purpose codec, standardised in 2012, and it is genuinely excellent — it scales from 6 kbps narrowband mono up to 510 kbps fullband stereo and it powers a large fraction of the world’s voice calls. Here is what the paper’s listening tests say about Opus when you squeeze it:

Codec & bitrateClean speechNoisy speechMusic (set 1)Music (set 2)
Reference (uncompressed)95.5 ±1.693.9 ±1.893.2 ±2.597.1 ±1.3
Opus @ 6 kbps30.1 ±2.819.1 ±5.920.6 ±5.817.9 ±5.3
Opus @ 12 kbps76.5 ±2.361.9 ±2.177.8 ±3.265.4 ±2.7
EnCodec @ 3 kbps67.0 ±1.562.5 ±2.389.6 ±3.187.8 ±2.9
EnCodec @ 6 kbps83.1 ±2.769.4 ±2.392.9 ±1.891.3 ±2.1

Read the music column twice. Opus at 6 kbps scores 20.6 on music — that is not "a bit muddy", that is a rating in the same neighbourhood as the deliberately-destroyed low anchor the listening protocol includes as a floor. EnCodec at half that bitrate scores 89.6. On music set 2, Opus at 12 kbps scores 65.4 and EnCodec at 3 kbps scores 87.8: EnCodec wins at a quarter of the bits.

Something structural is happening, not an incremental tuning win. A 69-point gap on a 100-point scale is the signature of a method that is solving a different problem than its baseline.

The honest counterweight, stated early. EnCodec is not uniformly dominant. On clean speech at 6 kbps it scores 83.1 while EVS — the 3GPP speech codec — scores 84.4 at 9.6 kbps, which is a fair fight EnCodec wins on bitrate but not on quality. And at 1.5 kbps EnCodec’s clean-speech score is 49.2, which is genuinely degraded audio. The revolution is at low bitrate on general audio, where hand-engineered codecs have their known blind spot. The appendix says this in as many words: existing codecs "support audio coding at low latency with high audio quality at low to medium bitrates (in the range of 12 to 24 kbps) but the audio quality deteriorates at very low bitrates (eg. 3 kbps) on non-speech audio."

The three components, named once

The paper’s entire system is three boxes and a training objective. Fix the names now; the rest of the lesson opens each box.

1. Encoder E
Takes the waveform x and outputs a continuous latent z. Streaming convolutions plus an LSTM. Chapter 2.
↓ z is continuous floats — you cannot transmit floats cheaply
2. Quantizer Q
Turns z into zq: a small set of integer indices into learnt codebooks. This is where the bits are decided. Chapters 3–4.
↓ indices are what actually travels over the wire
3. Decoder G
Reconstructs the time-domain waveform x̂ from zq. Mirrors the encoder with transposed convolutions. Chapter 2.
↓ trained end to end against…
4. The objective
Time-domain L1 + multi-scale mel + an adversarial discriminator + a commitment term, all held in proportion by a gradient balancer. Chapters 5–6.

Formally, the paper writes an audio signal of duration d as a tensor x in [−1, 1] of shape Ca × T, where Ca is the number of audio channels (1 for mono, 2 for stereo) and T = d · fsr is the number of samples at sample rate fsr. Every symbol in this lesson traces back to that line.

Data flow, with shapes

Architecture diagrams without tensor shapes are decoration. Here is the actual flow for one second of 24 kHz mono audio at 6 kbps, batch size B:

StageShapeTypeNote
input x[B, 1, 24000]float in [−1, 1]1 channel, 24,000 samples
after encoder E[B, D, 75]float32320× time downsample; D latent channels
after quantizer Q[B, Nq, 75]int in [0, 1023]Nq = 8 at 6 kbps; this is the bitstream
dequantized zq[B, D, 75]float32sum of the Nq selected codebook vectors
output x̂[B, 1, 24000]float in [−1, 1]320× upsample via transposed convs

Count the bits in row three and you get the codec’s bitrate with no hand-waving: 8 codebooks × 10 bits × 75 frames per second = 6000 bits per second. The compressed file is literally that integer tensor.

The single most useful reframe in this paper. Row three is a sequence of integers. That is exactly the shape of a sentence in a language model. Once your audio is a stream of tokens at 75 Hz, everything the sequence-modelling world knows how to do — autoregressive generation, conditioning, transfer — becomes available to audio. EnCodec was built as a codec; it was immediately adopted as a tokeniser. Chapter 9 follows that thread to AudioLM and MusicGen.

The budget arithmetic, three ways

Following the rule that every computation in this lesson appears as hand arithmetic first, then explicit code, then the compact form — here is the bit budget in all three.

By hand. You want 6 kbps at 24 kHz. The encoder gives you 75 frames per second, fixed by its strides. So each frame may cost 6000 ÷ 75 = 80 bits. Each codebook has 1024 entries, and naming one entry out of 1024 costs log2(1024) = 10 bits, because 210 = 1024. Therefore 80 ÷ 10 = 8 codebooks. Compression ratio versus the 384 kbps source: 384 ÷ 6 = 64×.

Step by step in code — the same arithmetic, nothing hidden:

python — the bit budget, spelled out
import math

sample_rate  = 24000          # Hz, EnCodec's mono setting
total_stride = 2 * 4 * 5 * 8   # the four encoder strides = 320
frame_rate   = sample_rate / total_stride       # 75.0 frames per second

codebook_size = 1024
bits_per_code = math.log2(codebook_size)        # 10.0 bits per codebook per frame

def bitrate_kbps(n_codebooks):
    bits_per_frame = n_codebooks * bits_per_code
    return frame_rate * bits_per_frame / 1000

for nq in [2, 4, 8, 16, 32]:
    print(nq, "codebooks ->", bitrate_kbps(nq), "kbps")

# 2  codebooks ->  1.5 kbps
# 4  codebooks ->  3.0 kbps
# 8  codebooks ->  6.0 kbps
# 16 codebooks -> 12.0 kbps
# 32 codebooks -> 24.0 kbps

The one-liner. Once you trust the pieces, the whole relationship collapses to a single expression:

python — one line
kbps = lambda nq, sr=24000, stride=320, bits=10: sr / stride * nq * bits / 1000

Invert it and you have the design question the rest of the paper answers. Given 80 bits per frame, which 80 bits? Any codec can spend the budget. The art is spending it on the parts of the signal a human will notice.

Notice what is missing from that arithmetic. Nowhere does the bitrate depend on the content. A silent frame costs 80 bits; a cymbal crash costs 80 bits. Classical codecs vary their bitrate with content, and EnCodec’s fixed-rate design initially throws that away. Chapter 7 wins it back with a Transformer that predicts the next codes, so predictable frames get cheap and surprising frames get expensive — recovering 25–40% of the bitrate.

What "MUSHRA" means, since every number here is one

Objective metrics for audio are famously unreliable, so the paper leans on human listening tests using the MUSHRA protocol (MUltiple Stimuli with Hidden Reference and Anchor). Annotators hear several versions of the same 5-second excerpt — the codecs under test, plus a hidden copy of the original, plus a deliberately degraded low anchor — and rate each from 1 to 100.

The hidden reference and low anchor are quality control, not content. If an annotator rates the untouched original below 90, or rates the mangled anchor above 80, they are not listening carefully and their data is discarded. The paper’s exact filters: remove annotators who rate the reference below 90 in at least 20% of cases, or rate the low anchor above 80 more than 50% of the time. They used 50 samples of 5 seconds per category with at least 10 annotations each.

So when you see "92.9 ±1.8", read it as: ten-plus screened humans, hearing this clip next to the original, put it at 92.9 on a scale where the original itself averages 93.2. That is the strongest form of evidence available in this field, and it is expensive, which is why the paper also reports two cheap objective metrics (ViSQOL and SI-SNR) for ablations — and why Chapter 5 will show you a case where those cheap metrics point the wrong way.

The road ahead

Chapters 1–2 — the machine
Why classical codecs stall, then EnCodec’s convolutional encoder/decoder with exact strides, frame rates, and the streaming trick that gets latency to 13 ms
Chapters 3–4 — the bits
Vector quantization from zero, then the showcase: residual vector quantization walked by hand and by simulation
Chapters 5–6 — the training
The four-term objective, the spectrogram-only discriminator, and the gradient balancer that makes the weights mean something
Chapters 7–9 — the payoff
Entropy coding with a tiny Transformer, the full MUSHRA verdict, and the lineage from EnCodec to the audio language models

One reading instruction for the whole lesson. Every time a number appears, it came from the paper — from Table 1, Table 2, Table 3, Table 4, Table 5, or the appendix tables A.2 through A.4. When a number is derived rather than quoted, the derivation is shown. Nothing here is decorative.

EnCodec at 24 kHz produces 75 latent frames per second. You are told a configuration transmits 40 bits per frame. What bitrate is that, and how many 1024-entry codebooks does it imply?
Opus at 6 kbps scores 20.6 on music while EnCodec at 3 kbps scores 89.6. Which reading of that gap is best supported by the paper?

Chapter 1: What a Codec Actually Is

You now know the budget: 80 bits per 13.3 ms at 6 kbps. Before watching a neural network spend it, watch how fifty years of signal processing spent it — because EnCodec is not a rejection of that tradition, it is a replacement of exactly one stage of it.

The paper compresses the whole tradition into one sentence: "Audio codecs typically employ a carefully engineered pipeline combining an encoder and a decoder to remove redundancies in the audio content and yield a compact bitstream. Traditionally, this is achieved by decomposing the input with a signal processing transform and trading off the quality of the components that are less likely to influence perception."

Unpack that into four stages. Every classical codec — MP3, AAC, Vorbis, Opus — is some arrangement of these:

Stage 1 — Transform
Move the waveform into a domain where energy is concentrated. Usually the MDCT: overlapping windows into frequency coefficients. A tonal sound that needed 1024 samples becomes a handful of large coefficients and a thousand near-zeros.
Stage 2 — Perceptual model
Decide which coefficients a human will not miss. This is the psychoacoustic model: masking curves derived from decades of listening experiments.
Stage 3 — Quantize
Round the surviving coefficients coarsely — coarsely enough that the rounding error hides under the masking curve. This is where the loss happens and where the bits are decided.
Stage 4 — Entropy code
Losslessly pack the quantized integers using their statistics: Huffman or arithmetic coding. No perceptual decisions here, just squeezing redundancy out of the symbol stream.

EnCodec keeps stages 3 and 4 nearly unchanged in spirit. It learns stage 1, and it replaces stage 2 with a trained discriminator. That is the whole conceptual move.

Masking: the idea that made MP3 possible

Stage 2 deserves a paragraph on its own, because it is genuinely beautiful engineering and because understanding it tells you exactly what a learned system has to reinvent.

Auditory masking is the fact that a loud sound makes nearby quieter sounds inaudible. Play a 1 kHz tone at 80 dB and a 1.1 kHz tone at 40 dB simultaneously, and you hear only the first. The second is masked. The masking effect spreads across frequency (asymmetrically — more upward than downward) and across time (a loud transient masks quiet sounds for a few milliseconds before it and tens of milliseconds after it).

So a psychoacoustic model computes, for each short frame, a masking threshold: a curve in dB across frequency, below which anything you add is inaudible. The quantizer then allocates bits so that the quantization noise sits just below that curve. Loud frequency regions get coarse quantization (their noise is hidden anyway); exposed quiet regions get fine quantization.

The engineering decision, made explicit. This is a hand-written prior about human hearing, calibrated on listening experiments from the 1970s and 80s, embedded permanently in a standard. It is extraordinarily good for the case it was tuned on — moderate bitrates, typical program material. It is a fixed prior, though, and a fixed prior cannot adapt to a signal type it never saw. That is the crack EnCodec grows in.
Sim 2 — Masking: how a classical codec decides what to throw away

The bars are the spectrum of one frame. Drag the masker’s loudness and frequency; the dashed curve is the resulting masking threshold. Components under the curve are discarded (they turn grey and their bits are freed). Push the bit budget down and watch the threshold get artificially raised until audible components start dying — that is what "Opus at 6 kbps" sounds like.

Masker level 78 dB
Masker freq band 8
Bit budget 70%

Masking, with numbers

The sim above is qualitative; do one bit-allocation by hand so the mechanism is not a black box. Consider a single frame with five frequency bands, their measured levels, and a masking threshold computed from a 78 dB masker sitting in band 2:

BandLevel (dB)Masking threshold (dB)Headroom = level − thresholdBits allocated
04230+122
15552+31
2 (masker)784
36165−40
44752−50

The rule of thumb every classical codec uses: quantization noise is roughly 6 dB below the signal for each bit you spend, so the bits you need in a band are the headroom divided by 6, rounded up:

bits(band) = max(0, ⌈ (level − threshold) / 6 ⌉)

Band 0: 12 / 6 = 2 bits. Band 1: 3 / 6 = 0.5, round up to 1 bit. Bands 3 and 4 have negative headroom — they sit under the mask, so anything you transmit there is inaudible next to the masker and gets zero bits. Total for this frame: 2 + 1 + 4 + 0 + 0 = 7 bits instead of the 20 a flat allocation would have spent.

That is the whole classical bargain, and it is a good one. But notice what it depends on: the threshold curve, which came from listening experiments on isolated tones, and the 6-dB-per-bit rule, which assumes the quantization noise is white and uncorrelated with the signal. Both assumptions weaken as the bitrate falls, because at very low rates the error is no longer a small perturbation — it is comparable to the signal itself, and "noise hidden under the mask" becomes "the signal has been replaced by something else."

This is the precise reason Opus scores 20.6 on music at 6 kbps. The psychoacoustic model is a local, additive theory of distortion: it tells you how much noise you may add before someone notices. At 6 kbps on music you cannot add a little noise; you must throw away most of the content. The model has no opinion about that regime, because nobody ran a listening experiment on "what if 90% of the spectrum is gone." A learned codec does not add noise at all — it resynthesises, from a code that was optimised end to end for exactly this budget. Different regime, different tool.

Here is the same allocation as code, so the comparison with the neural version later is concrete:

python — classical perceptual bit allocation, complete
import math

levels    = [42, 55, 78, 61, 47]     # dB per band, this frame
threshold = [30, 52, 0,  65, 52]     # from the psychoacoustic model

def bits_for(level, thresh):
    headroom = level - thresh
    return max(0, math.ceil(headroom / 6.0))   # ~6 dB of SNR per bit

alloc = [bits_for(l, t) for l, t in zip(levels, threshold)]
print(alloc, sum(alloc))    # [2, 1, 13, 0, 0]  -> masker band capped separately

# The entire perceptual model is the `threshold` list. It is a FIXED prior:
# the same curve shape for speech, for a snare drum, and for birdsong.

Read the last comment twice. Every line of that function is transparent, auditable, and standardised — which is exactly why Opus works identically on every device on earth and will still work in twenty years. The learned alternative buys quality at low bitrate and pays with opacity and a training distribution. That is not a small trade, and it is worth naming before we spend eight chapters admiring the learned side.

The parametric branch, and why it stalled

There is a second, older tradition that the paper cites in its first Audio Codec paragraph: parametric coding. Instead of transmitting a transform of the waveform, transmit the parameters of a model of how the sound was produced, and resynthesise at the far end.

For speech this is linear predictive coding, going back to Atal & Hanauer in 1971. Model the vocal tract as an all-pole filter, transmit the filter coefficients plus a description of the excitation (the buzz from the vocal folds or the hiss of a fricative), and rebuild the waveform. The filter coefficients are cheap. The result can run at extremely low bitrates.

The paper is blunt about the outcome: these methods have long been studied "but their quality has been severely limited. Despite some advances, modeling the excitation signal has remained a challenging task." The filter is easy; the excitation — the actual rich, noisy, non-stationary source signal — is where all the perceptual information lives, and hand-designed excitation models sound buzzy and synthetic.

Hold this thought, because it is the key to the whole neural-codec era. Parametric coding fails because synthesis is hard. Transform coding avoids synthesis entirely — it reconstructs by inverse transform — and so it never gets the extreme bitrate savings parametric coding promises. Neural vocoders (WaveNet, MelGAN, HiFi-GAN) solved synthesis. The moment synthesis became solved, the parametric dream became viable again, and a "neural codec" is exactly that: a learnt analysis producing a tiny parameter set, plus a learnt synthesiser good enough to make it sound real.

The two baselines you will see in every table

CodecStandardisedRangeDesigned for
OpusIETF, 20126 kbps narrowband mono → 510 kbps fullband stereoEverything. A hybrid of a speech coder (SILK) and a transform coder (CELT), switching or blending by bitrate and content.
EVS3GPP, 20145.9 to 128 kbps, audio bandwidth 4 kHz to 20 kHzVoice over LTE. Successor to AMR-WB. Speech-first, and it shows: EVS is the strongest baseline on clean speech in the paper.
MP3ISO, 1993Used at 64 kbps in the paper’s stereo tableThe stereo-music reference point. "Approximating the accuracy of certain components of sound that are considered to be beyond hearing capabilities of most humans" — the paper’s own description of masking.
Lyra v2Google, 20223.2 and 6 kbps in the paper’s testsThe neural baseline. It is the official SoundStream implementation, evaluated on audio upsampled to 32 kHz.

The neural lineage that leads directly to EnCodec

The paper’s related-work section is a short history you should be able to recite. Each entry fixed one thing and left one thing broken:

WorkThe moveWhat it left open
Morishima et al. 1990Neural networks as trained transforms in an encoder/decoderThree decades early; no compute, no synthesis quality
WaveNet (Oord et al. 2016)Autoregressive raw-waveform synthesis that finally sounded realSample-by-sample generation — hopelessly slow for a codec
LPCNet in a codec (Valin & Skoglund 2019)Condition a fast neural vocoder on hand-crafted features + a uniform quantizerFeatures still hand-designed; not end to end
VQ-VAE + WaveNet (Gârbacea et al. 2019)Discrete units from a VQ-VAE, decoded by WaveNetA single codebook caps the achievable rate; slow decoder
GAN vocoders (MelGAN, HiFi-GAN, 2019–2020)Multi-scale and multi-period adversarial losses give WaveNet quality at feed-forward speedVocoders, not codecs — no learnt discrete bottleneck
SoundStream (Zeghidour et al. 2021)The direct ancestor: fully convolutional encoder/decoder + residual vector quantization, reconstruction + adversarial lossesComplicated discriminator stack; hand-tuned loss weights; the things EnCodec simplifies

Read the last two rows together and EnCodec’s contribution list becomes obvious. SoundStream established the recipe. EnCodec asks: can we make it simpler (one discriminator family instead of two), more stable (the balancer), and smaller on the wire (entropy coding)? The paper’s own contribution list is precisely those three plus the extensive MUSHRA study.

What the paper does not claim. EnCodec does not invent RVQ — that is Gray 1984 as a technique and Zeghidour 2021 in this setting. It does not invent adversarial audio losses, feature matching, straight-through estimation, or arithmetic coding. Its novelty is a specific, well-argued combination, plus one genuinely new mechanism (the balancer) and one careful engineering study (which discriminator actually matters). Systems papers are often like this, and reading them well means separating "new" from "assembled well" without treating the second as a criticism.

Why a learned transform can beat a designed one

Here is the argument, stated carefully, because "neural networks are better" is not an argument.

The MDCT is a fixed linear transform. It is optimal for signals that are locally stationary and sinusoidal, which describes a lot of music and very little of anything else. When the signal is a transient, a plosive, a room reverberation tail, or three sources mixed together, the MDCT’s energy compaction degrades: the "handful of large coefficients" becomes dozens, and the bit budget explodes.

A learned encoder is a nonlinear, data-adaptive transform. It can devote latent dimensions to whatever structures actually recur in its training distribution — and EnCodec’s training distribution is deliberately enormous and mixed: speech, noisy speech, music, general environmental audio, plus on-the-fly mixtures of two or three sources (Chapter 8 has the exact sampling probabilities). Whatever regularities exist in that distribution become cheap to encode.

The cost is equally real, and the paper names it in the introduction: "the model has to represent a wide range of signals, such as not to overfit the training set or produce artifact laden audio outside its comfort zone." A fixed transform has no comfort zone; a learned one does. The paper’s two answers are a large and diverse training set, and discriminator networks acting as perceptual losses.

The trade, in one line. Classical codecs are uniformly mediocre and never surprise you. Neural codecs are excellent inside their distribution and can fail strangely outside it. Choose accordingly — and note that this is the same trade you accept every time you deploy any learned system.

Concept check before you move on

Suppose you took EnCodec and swapped its learnt encoder for a plain MDCT, keeping the RVQ quantizer and the neural decoder. Would it still work?

Partly. You would keep the powerful synthesiser and the strong discrete bottleneck, so it would beat a classical codec at the same bitrate on synthesis quality. But you would lose the adaptivity: the MDCT coefficients for a hard signal are high-dimensional and poorly clustered, so the codebooks would have to cover a much messier space and the residual would fall more slowly with each stage (Chapter 4 makes "falls more slowly" a measurable thing). The encoder’s job is not just to transform — it is to produce a latent whose geometry is friendly to quantization. That is a joint-training effect and it is why the whole system is trained end to end.

Which stage of the classical four-stage pipeline does EnCodec most directly replace with a trained component?
The paper says parametric speech codecs were held back because "modeling the excitation signal has remained a challenging task." Why does that history matter for neural codecs?

Chapter 2: The Encoder and the Decoder

Time to open box 1 and box 3. The paper describes them in a single dense paragraph; we will spend a chapter unfolding it, because every constant in it is load-bearing and several of them explain results four chapters later.

Here is the paragraph, quoted, so you can check the unfolding against the source:

The paper, Section 3.1. "The encoder model E consists in a 1D convolution with C channels and a kernel size of 7 followed by B convolution blocks. Each convolution block is composed of a single residual unit followed by a down-sampling layer consisting in a strided convolution, with a kernel size K of twice the stride S. The residual unit contains two convolutions with kernel size 3 and a skip-connection. The number of channels is doubled whenever down-sampling occurred. The convolution blocks are followed by a two-layer LSTM for sequence modeling and a final 1D convolution layer with a kernel size of 7 and D output channels. … we use C = 32, B = 4 and (2, 4, 5, 8) as strides."

Walking the encoder, layer by layer, with shapes

Substitute C = 32, B = 4, strides (2, 4, 5, 8), and one second of 24 kHz mono audio. Track two things at every step: the number of channels, and the number of time steps.

#LayerChannelsTime stepsWhy
0input waveform124000mono, in [−1, 1]
1Conv1d, kernel 71 → 3224000stride 1: lift to C channels without touching time
2residual unit (2 convs, k=3, skip)3224000local nonlinear processing at full rate
3strided conv, S=2, K=432 → 6412000first downsample; channels double
4residual unit6412000
5strided conv, S=4, K=864 → 1283000
6residual unit1283000
7strided conv, S=5, K=10128 → 256600
8residual unit256600
9strided conv, S=8, K=16256 → 512752·4·5·8 = 320 total; 24000/320 = 75
10two-layer LSTM51275sequence modelling over the latent, shapes unchanged
11Conv1d, kernel 7512 → D75project to the latent dimension the quantizer will see

Row 9 is the punchline: 75 latent steps per second at 24 kHz, and 150 at 48 kHz (48000 / 320 = 150). The paper states both figures explicitly and uses the same architecture for both sample rates — the frame rate simply doubles.

Note the channel/time trade in the middle rows. Time steps fall by 320× while channels rise by 16× (32 → 512). The total activation volume shrinks by 20× through the encoder, which is the compression happening before a single bit is spent. The quantizer inherits an already-compact representation.

Why K = 2S, always. A strided convolution with kernel size exactly twice its stride gives each output step a receptive field that covers its own S input samples plus the S before it — adjacent output steps overlap by exactly one stride. Set K = S and the windows tile without overlap, which produces audible blocking artifacts at the seams (this is the same reason overlap-add windows exist in the STFT). Set K much larger than 2S and you pay compute for redundancy. K = 2S is the minimal overlap that avoids seams — and on the decoder side, transposed convolutions with K = 2S are what let the streaming buffer trick in the next section work so cleanly.
Sim 3 — The architecture, with live tensor shapes

Click any block to inspect it. The bar heights encode time steps (log scale) and the bar widths encode channels, so you can see the representation trading time resolution for channel depth and then mirroring back. Toggle the LSTM off to see the ablation from Table A.3, and switch the sample rate to watch the frame rate double.

The decoder is the encoder, run backwards

"The decoder mirrors the encoder, using transposed convolutions instead of strided convolutions, and with the strides in reverse order as in the encoder, outputting the final mono or stereo audio."

So the decoder strides are (8, 5, 4, 2). Starting from [B, D, 75] the transposed convolutions upsample by 8, then 5, then 4, then 2 — back to 24000 — while channels halve at each stage and a final kernel-7 convolution produces Ca output channels. A two-layer LSTM sits on the decoder side too: the paper says the sequential modelling component is applied over the latent representation "both on the encoder and on the decoder side."

Activation is ELU throughout, and normalisation is either layer normalisation or weight normalisation — and which one is not a stylistic choice. It is the hinge on which the streaming variant turns.

The two variants: streamable and non-streamable

This is the subtlest engineering in the paper and it is worth slowing down for, because it is the difference between a codec you can use in a phone call and one you can only use for archiving.

A convolution needs context on both sides of the sample it is producing. In the ordinary (non-causal) setup you pad symmetrically: for a total padding of K − S, you split it equally before the first time step and after the last one, with one extra before if K − S is odd. Output step t then depends on input samples both before and after t. That is fine offline. In a live stream it is fatal — you would have to wait for the future.

Non-streamable
Padding K−S split equally before and after → symmetric receptive field. Input further split into 1-second chunks with 10 ms overlap to avoid clicks, each chunk normalised before the model and un-normalised after (the scale is transmitted, "adding a negligible bandwidth overhead"). Uses layer normalization with statistics computed including the time dimension, which keeps relative scale information.
↓ the same network, only padding and normalisation change
Streamable
All padding goes before the first time step → strictly causal. For a transposed convolution with stride s, output the first s time steps and keep the remaining s in memory, completing them when the next frame arrives (or discarding at end of stream). Layer normalization is replaced by weight normalization, since time-dimension statistics are ill-suited to streaming.

The payoff of the causal padding scheme is stated exactly: "the model can output 320 samples (13 ms) as soon as the first 320 samples (13 ms) are received." One frame in, one frame out. Table 5 reports the initial latency of the 24 kHz streaming model as 13.3 ms, and the 48 kHz non-streaming version at 1 second — that whole second is the 1-second chunk needed to compute the normalisation statistics.

The normalisation detail is the latency. Read that again: the 48 kHz model’s one-second latency is not compute, and not receptive field. It is that you cannot normalise a chunk until you have seen the whole chunk. A single design decision made for numerical reasons — keeping relative scale information via time-inclusive layer norm — costs 75× more latency than the entire rest of the network. This is the sort of thing that only shows up when you build the system, and it is why the streamable variant swaps to weight normalisation, which depends on weights rather than on activations and therefore needs no future samples at all.

What does streaming cost in quality? Table 3, at 6 kbps on an equal mix of speech and music:

ModelStreamableSI-SNRViSQOL
Opusyes2.452.60
EVSyes1.892.74
EnCodecyes6.674.35
EnCodecno7.464.39

Streaming costs 0.79 dB of SI-SNR and 0.04 ViSQOL. The paper’s verdict: "we notice a small degradation switching from non-streamable to streamable but the performance remains strong while this setting enables streaming inference." Meanwhile both EnCodec variants sit around 4.35 ViSQOL where the classical codecs sit at 2.6–2.7 — the streaming penalty is a rounding error next to the method gap.

Sim 4 — Causal padding and the 13 ms latency

Press play and watch samples arrive left to right. In non-streamable mode the receptive field straddles the current position, so the first output cannot be emitted until future samples exist — the red "waiting" region. In streamable mode all padding sits on the left, the receptive field is one-sided, and output emerges one 320-sample frame behind the input. The transposed-convolution buffer (the half-frame kept in memory) is drawn explicitly.

The latency budget, added up

"13 ms" is quoted so often that it is worth deriving, because latency in a real call is a sum of several things and only one of them is the codec’s algorithmic delay.

Component24 kHz streamingWhere it comes from
Frame size320 samples = 13.3 msThe total stride. You cannot emit an output until a whole frame of input exists.
Look-ahead0 msAll padding is on the left, so no future samples are needed. This is the causal-padding payoff.
Normalisation window0 msWeight normalization depends on weights, not on activations. The non-streamable variant pays 1 s here.
Compute≈ 1.3 ms per frameRTF 10 means 13.3 ms of audio takes about 1.3 ms to process.
Entropy coding+13 ms if enabledThe stream cannot be flushed each frame, so decoding frame t needs frame t+1 partially received.
Total, no entropy coding≈ 14.6 msWell inside the ~20 ms budget conversational audio wants.

Compare the alternatives on that axis alone. Opus at its default runs 20 ms frames plus 2.5 ms look-ahead. MP3 needs a 1152-sample granule — 26 ms at 44.1 kHz — plus the MDCT overlap. EnCodec’s 13.3 ms is competitive with the best low-latency configurations of either, which is remarkable for a neural system and is entirely due to the padding scheme in the previous section.

Why frame size is the floor, and what it would cost to lower it. Latency and frame rate are the same knob. Drop the total stride from 320 to 160 and latency halves to 6.7 ms — but the frame rate doubles to 150 Hz, so at a fixed number of codebooks the bitrate doubles too. To hold 6 kbps you would have to halve Nq to 4, and Chapter 4’s ladder says 4 codebooks is the 3 kbps operating point in quality terms. Latency, bitrate, and quality are three faces of one budget, and 320 is where the authors chose to stand.

Ablating the architecture: what each piece is worth

Table A.3 in the appendix takes the base streamable 24 kHz model at 6 kbps and changes one thing at a time. RTF is the real-time factor: the ratio of audio duration to processing time, so RTF > 1 means faster than real time. Profiled on a single thread of a 2019 MacBook Pro CPU.

VariantRTF encodeRTF decodeSI-SNRViSQOL
EnCodec base (C=32, 1 res unit, LSTM)9.810.46.674.35
Channels = 1626.025.76.404.32
Channels = 641.33.16.704.38
norm = None10.110.46.454.29
LSTM = 015.014.66.404.35
Residual layers = 3, LSTM = 06.07.36.324.35

Three readings, in order of how much they teach:

1. Capacity has brutally diminishing returns. Doubling channels from 32 to 64 buys 0.03 dB of SI-SNR and 0.03 ViSQOL — and costs 7.5× the encoding speed (9.8 → 1.3). Halving to 16 loses 0.27 dB and triples the speed. The paper’s phrasing: "increasing the capacity of the model only marginally affects the scores on objective metrics while it has a high impact on the real-time factor." C = 32 is the knee of the curve.

2. The LSTM is cheap quality. Removing it gains 53% encoding speed and loses 0.27 dB. Replacing it with extra residual units (3 res layers, no LSTM) is worse on both axes: 6.32 SI-SNR and slower than the base model. Recurrence over 75 latent steps per second is a far better use of parameters than more convolution at the waveform rate — because at 75 Hz the LSTM is doing long-range work that a stack of small kernels would need enormous depth to reach.

3. Some normalisation beats none. "norm = None" costs 0.22 dB and 0.06 ViSQOL for essentially no speed gain. The paper notes exactly this: "We notice a small gain over the objective metrics by keeping a form of normalization."

How to read an ablation table like an engineer. Do not scan for the biggest number. Scan for the cheapest improvement and the most expensive one, and check whether any row is dominated on both axes. Here, "Residual = 3, LSTM = 0" is dominated: it is slower and worse than base. That row exists in the paper to close off an obvious reviewer question ("why not just use more convolutions?") with data rather than argument. Papers that include their dominated variants are papers that actually ran the experiments.

The residual unit in code

Every convolution block is one residual unit followed by one downsample. The residual unit is small enough to write out completely:

python — the residual unit and one encoder block
import torch.nn as nn

class ResidualUnit(nn.Module):
    # "two convolutions with kernel size 3 and a skip-connection"
    def __init__(self, ch):
        super().__init__()
        self.block = nn.Sequential(
            nn.ELU(),
            nn.Conv1d(ch, ch, kernel_size=3, padding=1),   # causal in the streaming variant
            nn.ELU(),
            nn.Conv1d(ch, ch, kernel_size=3, padding=1),
        )
    def forward(self, x):
        return x + self.block(x)          # the skip connection

class EncoderBlock(nn.Module):
    # "a single residual unit followed by a down-sampling layer ...
    #  kernel size K of twice the stride S ... channels doubled"
    def __init__(self, ch_in, stride):
        super().__init__()
        self.res  = ResidualUnit(ch_in)
        self.down = nn.Conv1d(ch_in, 2 * ch_in,
                              kernel_size=2 * stride, stride=stride)
    def forward(self, x):
        return self.down(nn.functional.elu(self.res(x)))

# the whole encoder, exactly as the paper specifies it
strides = (2, 4, 5, 8)                 # product = 320
C, D = 32, 128          # paper fixes C=32; it writes the latent width only as "D"
layers = [nn.Conv1d(1, C, kernel_size=7, padding=3)]
ch = C
for s in strides:
    layers.append(EncoderBlock(ch, s)); ch *= 2   # 32->64->128->256->512
layers += [LSTMWrapper(ch, num_layers=2),
           nn.Conv1d(ch, D, kernel_size=7, padding=3)]
encoder = nn.Sequential(*layers)

The causal variant differs only in where the padding goes: replace each symmetric padding=p with a left-only pad of K − S applied before the convolution. That single change — plus swapping layer norm for weight norm — converts the offline model into the 13 ms streaming model. The weights have the same shapes; the graph has the same layers.

Why does the 48 kHz non-streamable model have 1 second of initial latency while the 24 kHz streamable model has 13.3 ms?
Table A.3 shows "Residual layers = 3, LSTM = 0" at RTF 6.0/7.3 and SI-SNR 6.32, versus base at 9.8/10.4 and 6.67. What is the lesson?

Chapter 3: Vector Quantization, Built From Zero

The encoder hands you a tensor of shape [B, D, 75] full of 32-bit floats. If you transmitted it as-is, one second of mono audio at D = 128 would cost 128 × 75 × 32 = 307,200 bits — 307 kbps, barely better than the raw waveform. The encoder compressed the volume; it did nothing about the precision.

So the real question of this chapter: how do you turn a continuous vector into a small integer, cheaply, and in a way that a gradient can flow back through?

Scalar quantization, and why it is not enough

The obvious answer is to round each of the D numbers to a grid. With b bits per dimension you pay D · b bits per frame. At D = 128 and even a stingy b = 1 you are at 128 bits per frame — 9.6 kbps — and one bit per dimension is a catastrophically coarse grid.

Scalar quantization also throws away every correlation. If two latent dimensions always move together, a per-dimension grid spends bits describing both independently. Real latents are full of such structure; that is what makes them a good representation in the first place.

The paper tried this, and reports it honestly. Appendix A.2.1 describes a full scalar-quantization alternative called DiffQ, which uses pseudo-quantization noise at train time (additive uniform noise scaled by 2−B) so the operation is differentiable, plus a learnt per-dimension bit allocation B and a penalty that pushes the estimated bandwidth toward a target. It works. Table A.2 puts EnCodec-with-DiffQ at 72.3 MUSHRA at 3 kbps and EnCodec-with-RVQ at 76.8. The paper’s summary of its alternative quantizers: "we found in preliminary results that they provide similar or worse results." That is what a fair negative result looks like.

Vector quantization: name a point, not a coordinate

The alternative is to quantize the whole vector at once. Keep a codebook: a list of N learnt vectors c0, …, cN−1, each in RD. To encode a latent z, find the nearest codebook entry and transmit its index:

q(z) = ck  where  k = argminj ‖z − cj2

The cost is log2(N) bits regardless of D. That is the entire magic: dimensionality becomes free. A 128-dimensional vector and a 2-dimensional vector both cost 10 bits if the codebook has 1024 entries. What you pay for instead is coverage — the codebook must contain a point near wherever your latents actually live.

Geometrically, a codebook partitions RD into N Voronoi cells: the set of points closer to ck than to any other entry. Encoding is "which cell am I in"; decoding is "here is that cell’s representative point". The quantization error is the vector from z to its cell’s centre, and the codebook is good exactly when those errors are small on the distribution you care about.

Sim 5 — The Voronoi playground: what a codebook actually does

A 2-D stand-in for the 128-D latent space. Drag the white-hot point anywhere; the highlighted cell is the codebook entry that wins, and the arrow is the quantization error you cannot transmit. Add entries and watch the cells shrink — then read the bits counter, which grows only as log2(N). The "cluster the data" button runs a few steps of k-means so you can see a trained codebook beat a random one.

Codebook size 8

The wall: why one codebook cannot reach 24 kbps

Now do the arithmetic that forces the rest of the paper. You want 24 kbps at 75 frames per second, so 320 bits per frame. With a single codebook that means:

N = 2320 ≈ 2.1 × 1096 entries

There are roughly 1080 atoms in the observable universe. Even at the modest 6 kbps target you would need 280 ≈ 1.2 × 1024 entries, each a D-dimensional float vector. Three separate walls:

WallWhy
MemoryStoring N × D floats. At N = 280 and D = 128, the codebook is larger than every hard drive ever manufactured, by a factor with 15 digits in it.
SearchEncoding requires a nearest-neighbour search over N entries per frame, 75 times a second, in real time on one CPU core.
Training dataEvery entry needs enough assigned samples to estimate it. With N entries you need far more than N training vectors. At N = 280 the universe does not contain enough audio.

This is the moment the reader should feel the need for the next chapter. A single codebook is a flat code: one index, one point. To get to hundreds of bits per frame you need a code whose effective size grows multiplicatively while its storage grows additively. That is exactly what residual vector quantization is, and it is Chapter 4.

How good can a codebook be? The rate–distortion view

Before fixing VQ’s training problems, it helps to know what a perfect codebook would achieve, so you can tell whether your codebook is bad or the problem is hard.

Suppose your latents fill a D-dimensional region of volume V roughly uniformly. A codebook of N entries partitions that region into N cells, so each cell has volume about V/N. A cell of volume v in D dimensions has a characteristic radius proportional to v1/D. Therefore:

typical quantization error ∝ (V / N)1/D = V1/D · N−1/D

Substitute N = 2b for b bits and take logarithms:

error ∝ 2−b/D  ⇒   error in dB falls by about 6/D dB per bit

Read that exponent carefully, because it is the whole story of this chapter. The error falls with b/D, not b. Halving the error needs D extra bits, not one. At D = 128, buying a factor of two in accuracy costs 128 bits per frame — 9.6 kbps of your budget for one halving.

Why this is not as bleak as it looks, and why the encoder matters more than the codebook. That formula assumed the latents fill a D-dimensional region. Real latents do not: a trained encoder concentrates them on a much lower-dimensional structure inside RD. If the effective dimension is d < D, the exponent becomes b/d and every bit buys far more. So the encoder’s real job is not only to compress time — it is to make the latent distribution as low-dimensional and as tightly clustered as it can, because that is what converts bits into accuracy. This is the deepest reason the whole system is trained end to end rather than fitting a codebook to a fixed encoder: the encoder learns to produce latents that are easy to quantize.

You can watch this exact effect in the Voronoi sim: cluster the codebook onto the data (k-means) and the mean error drops sharply at the same N, because the entries stop wasting themselves on empty space. Randomly scattered entries behave as if the effective dimension were much higher than it is.

Three problems VQ has, and the three fixes EnCodec uses

Before stacking codebooks, the single-codebook version must actually train. Three things go wrong, and the paper fixes each with a named technique. It follows the same procedure as Dhariwal et al. (Jukebox) and Zeghidour et al. (SoundStream).

Problem 1: argmin has no gradient. The map z → ck is piecewise constant. Its derivative is zero almost everywhere and undefined on the cell boundaries. Backpropagate through it honestly and the encoder receives nothing at all.

Fix 1: the straight-through estimator. Quoting the paper: "We use a straight-through-estimator to compute the gradient of the encoder, e.g. as if the quantization step was the identity function during the backward phase." Forward, you quantize. Backward, you pretend you did not. In code this is a one-line trick:

python — straight-through in one line
z_q = z + (quantize(z) - z).detach()
# forward:  z + (q - z) == q          -> the decoder sees the quantized vector
# backward: d/dz of the detached term is 0, so grad flows straight to z

It is a biased estimator — the true gradient of a step function is not the identity — but the bias is small when quantization error is small, which is precisely the regime the commitment loss enforces.

Problem 2: the encoder can run away from the codebook. Nothing in the reconstruction loss stops the encoder from drifting its outputs to a region no codebook entry covers. The straight-through gradient does not feel quantization error at all, so the encoder is free to make that error enormous.

Fix 2: the commitment loss. "A commitment loss, consisting of the MSE between the input of the quantizer and its output, with gradient only computed with respect to its input, is added to the overall training loss." So it penalises the encoder for producing latents far from the codebook, while leaving the codebook itself untouched by this term (the codebook has its own update rule — fix 3). The name is apt: the encoder must commit to the code it triggered.

Problem 3: codebook collapse. Entries that start far from the data are never selected, so they are never updated, so they stay far from the data forever. A 1024-entry codebook can silently degenerate into a 40-entry one, and your 10 bits per frame become 5.3.

Fix 3: EMA updates plus dead-entry restarts. "The codebook entry selected for each input is updated using an exponential moving average with a decay of 0.99, and entries that are not used are replaced with a candidate sampled from the current batch." The EMA makes each entry drift toward the running mean of the vectors assigned to it — a soft, online k-means. The restart is the crucial half: any entry that goes unused gets teleported onto a real data point from the current batch, where it will immediately win some assignments.

Why EMA and not gradient descent on the codebook? Because the ideal codebook entry is the mean of the vectors assigned to it — that is the closed-form minimiser of squared error within a cell. An EMA with decay 0.99 estimates that mean directly, at a rate set by the data rather than by a learning rate that must be tuned against every other loss in the system. It is k-means run online, and it sidesteps a hyperparameter interaction that would otherwise land in the balancer’s lap in Chapter 6.

Worked example: quantizing one vector by hand

Take D = 2 so the arithmetic is visible, and a tiny codebook of four entries:

c0 = (1.0, 0.0)   c1 = (0.0, 1.0)   c2 = (−1.0, 0.0)   c3 = (0.0, −1.0)

and the latent z = (0.9, −0.4). Compute all four squared distances, every term shown:

‖z − c02 = (0.9 − 1.0)2 + (−0.4 − 0.0)2 = 0.01 + 0.16 = 0.17
‖z − c12 = (0.9 − 0.0)2 + (−0.4 − 1.0)2 = 0.81 + 1.96 = 2.77
‖z − c22 = (0.9 + 1.0)2 + (−0.4 − 0.0)2 = 3.61 + 0.16 = 3.77
‖z − c32 = (0.9 − 0.0)2 + (−0.4 + 1.0)2 = 0.81 + 0.36 = 1.17

The minimum is 0.17, at index 0. So the transmitted symbol is the integer 0, costing log2(4) = 2 bits, and the decoder reconstructs c0 = (1.0, 0.0).

The error vector is z − c0 = (0.9 − 1.0, −0.4 − 0.0) = (−0.1, −0.4), with length √0.17 = 0.4123. For scale, ‖z‖ = √(0.81 + 0.16) = √0.97 = 0.9849, so we have kept the vector to within 41.9% relative error. That is what 2 bits buys.

The commitment loss contribution from this one vector is exactly the squared distance we already computed: ‖z − q(z)‖22 = 0.17.

Hold onto the error vector (−0.1, −0.4). In Chapter 4 we will quantize it, with a second codebook, and watch 41.9% become 10.2%.

The same computation, three ways

python — step by step, exactly the hand arithmetic
import numpy as np

z  = np.array([0.9, -0.4])
cb = np.array([[1.0, 0.0],
               [0.0, 1.0],
               [-1.0, 0.0],
               [0.0, -1.0]])

d2 = []
for c in cb:
    diff = z - c
    d2.append(diff[0]**2 + diff[1]**2)
print(d2)                # [0.17, 2.77, 3.77, 1.17]

k = int(np.argmin(d2))   # 0
q = cb[k]                # [1.0, 0.0]
err = z - q              # [-0.1, -0.4]
print(k, q, err, np.linalg.norm(err))   # 0 [1. 0.] [-0.1 -0.4] 0.41231
python — vectorised, the form you would actually ship
# distances for a whole batch Z of shape [M, D] against codebook [N, D]
d2 = ((Z[:, None, :] - cb[None, :, :])**2).sum(-1)      # [M, N]
idx = d2.argmin(-1)                                       # [M]
q   = cb[idx]                                              # [M, D]
python — the one-liner, using the expansion of the square
# ||z-c||^2 = ||z||^2 - 2 z.c + ||c||^2 ; ||z||^2 is constant per row, so drop it
idx = (cb @ Z.T * 2 - (cb**2).sum(-1, keepdims=True)).argmax(0)

That last form is what real implementations use: one matrix multiply instead of an M × N × D broadcast, which matters when N = 1024 and you are running 75 times a second on one CPU core.

Concept + realization check. Trace the data types through the quantizer once, slowly. In: z, float32, shape [B, D, T]. Out of the argmin: idx, int64, shape [B, T] — this is the only thing that travels over the wire. Out of the lookup: z_q, float32, shape [B, D, T]. The decoder never sees an integer; the network never sees a bitstream. The integer tensor is the codec, and everything on either side of it is a differentiable function that exists only to make that integer tensor meaningful.
A single 1024-entry codebook costs 10 bits per frame. To reach 24 kbps at 75 frames/s you need 320 bits per frame. What goes wrong if you simply enlarge the codebook to 2320 entries?
EnCodec replaces unused codebook entries "with a candidate sampled from the current batch." What failure does that prevent, and why is EMA alone not enough?

Chapter 4: Showcase — The Residual Vector Quantization Walk

Chapter 3 left you with an error vector: after spending 2 bits on z = (0.9, −0.4) we were still off by (−0.1, −0.4). The obvious question — so obvious that it took the field from 1984 to 2021 to make it work in a neural codec — is: why not quantize the error too?

That is residual vector quantization, in one sentence. The paper’s version: "Vector quantization consists in projecting an input vector onto the closest entry in a codebook of a given size. RVQ refines this process by computing the residual after quantization, and further quantizing it using a second codebook, and so forth."

r1 = z
for c = 1 … Nq:   kc = argminj ‖rc − Cc[j]‖2,   rc+1 = rc − Cc[kc]
ẑ = C1[k1] + C2[k2] + … + CNq[kNq]

Each stage has its own codebook, trained on the residuals that reach it. Stage 1’s codebook learns the coarse shape of the latent distribution. Stage 2’s learns the shape of stage 1’s mistakes. Stage 3’s learns the shape of stage 2’s mistakes. It is boosting, applied to quantization.

The multiplicative miracle

Here is why this breaks the wall from Chapter 3. With Nq codebooks of N entries each, the set of reachable reconstruction points is every possible sum of one entry from each codebook:

effective centroids = NNq       stored vectors = Nq × N

Multiplicative in the exponent, additive in storage. Put EnCodec’s numbers in: N = 1024, Nq = 32.

QuantitySingle codebookRVQ (32 × 1024)
Bits per frame320320
Reachable points2320 ≈ 1096102432 = 2320 ≈ 1096
Vectors you must store109632,768
Distance computations per frame109632 × 1024 = 32,768

Both codes address the same number of points. One requires more storage than there is matter; the other fits in 32,768 × D floats — about 16 MB at D = 128 in float32 — and searches in 32 × 1024 = 32,768 distance evaluations per frame, which a CPU does comfortably 75 times a second.

What you give up, stated honestly. Those 1096 reachable points are not freely placed. They are constrained to be sums drawn from 32 fixed sets, which is an enormously smaller family than "any 1096 points in RD". So RVQ is strictly weaker than an unconstrained code of the same bit count. The bet is that the latent distribution is approximately decomposable this way — that coarse structure plus successive refinements is a good model of where audio latents live. It is a bet, and the encoder is trained jointly, which means the encoder learns to produce latents for which the bet is true. That is the part people miss.

Hand-worked: two stages, every intermediate number

Pick up exactly where Chapter 3 stopped. z = (0.9, −0.4), and stage 1 uses the same four-entry codebook.

Stage 1. Residual entering the stage is r1 = z = (0.9, −0.4). Codebook C1:

C1[0] = (1.0, 0.0)   C1[1] = (0.0, 1.0)   C1[2] = (−1.0, 0.0)   C1[3] = (0.0, −1.0)

Squared distances, recomputed so this chapter stands alone:

to C1[0]: (0.9−1.0)2 + (−0.4−0.0)2 = 0.01 + 0.16 = 0.17  ← winner
to C1[1]: (0.9−0.0)2 + (−0.4−1.0)2 = 0.81 + 1.96 = 2.77
to C1[2]: (0.9+1.0)2 + (−0.4−0.0)2 = 3.61 + 0.16 = 3.77
to C1[3]: (0.9−0.0)2 + (−0.4+1.0)2 = 0.81 + 0.36 = 1.17

So k1 = 0, and the new residual is:

r2 = r1 − C1[0] = (0.9 − 1.0,  −0.4 − 0.0) = (−0.1, −0.4),   ‖r2‖ = √0.17 = 0.4123

Stage 2. Its codebook C2 was trained on residuals, so its entries are small — that is the crucial structural fact, and it is why stage 2 cannot simply be a copy of stage 1:

C2[0] = (0.2, 0.1)   C2[1] = (−0.1, −0.3)   C2[2] = (0.0, 0.4)   C2[3] = (−0.3, 0.05)

Squared distances from r2 = (−0.1, −0.4), every term written out:

to C2[0]: (−0.1−0.2)2 + (−0.4−0.1)2 = 0.09 + 0.25 = 0.34
to C2[1]: (−0.1+0.1)2 + (−0.4+0.3)2 = 0.00 + 0.01 = 0.01  ← winner
to C2[2]: (−0.1−0.0)2 + (−0.4−0.4)2 = 0.01 + 0.64 = 0.65
to C2[3]: (−0.1+0.3)2 + (−0.4−0.05)2 = 0.04 + 0.2025 = 0.2425

So k2 = 1, and:

r3 = r2 − C2[1] = (−0.1 − (−0.1),  −0.4 − (−0.3)) = (0.0, −0.1),   ‖r3‖ = 0.1

The reconstruction. The decoder receives the two integers (0, 1) and sums the corresponding entries — it never sees a residual:

ẑ = C1[0] + C2[1] = (1.0, 0.0) + (−0.1, −0.3) = (0.9, −0.3)

Check against the true z = (0.9, −0.4): the error is (0.0, −0.1), norm 0.1 — which is exactly r3, as it must be. The final residual is the reconstruction error. That identity is the whole reason the algorithm is written as a loop over residuals.

The scoreboard. Relative error against ‖z‖ = 0.9849:

StageBits spentResidual normRelative errorImprovement
0 (nothing sent)00.9849100%
120.412341.9%2.39×
240.100010.2%4.12×

Two bits took you from 100% error to 41.9%. Two more took you from 41.9% to 10.2%. Each stage cut the residual by roughly a factor of four, using the same number of bits — because each stage’s codebook is scaled to the residuals it actually receives.

The invariant that makes RVQ work at all. Every stage solves the same problem at a smaller scale. Stage 1 quantizes vectors of typical length 1; stage 2 quantizes vectors of typical length 0.4; stage 3 quantizes vectors of typical length 0.1. If you gave stage 2 a copy of stage 1’s codebook, every entry would overshoot the residual wildly and the "refinement" would make things worse. This is why the codebooks are trained separately, on the residual stream that actually reaches them, and why you cannot swap the order of two stages after training.

And the commitment loss for this frame, from Equation 3 — the sum over residual steps of the squared distance from the residual to its chosen entry, which are the two winning distances we already computed:

w = ‖r1 − q1(r1)‖22 + ‖r2 − q2(r2)‖22 = 0.17 + 0.01 = 0.18

THE SHOWCASE — walk the residual yourself

Sim 6 — The RVQ walk: a real vector, stage by stage

The left panel is the 2-D latent space. The target vector is drawn from the origin; each stage picks its nearest codebook entry (highlighted), draws the arrow it contributes, and moves the running reconstruction closer. The dashed vector is the current residual — the thing the next stage will try to cancel. Drag the target anywhere to re-run the walk on a new vector. The right panel plots the residual norm falling stage by stage on a log axis, which is where the geometric decay becomes obvious.

Stages shown 2

Two behaviours to notice while you play. First, the arrows get shorter every stage — geometric decay, roughly a constant factor per stage, which on the log plot is a straight line. Second, drag the target far outside the data cloud and the decay stalls: the codebooks were trained on residuals from a particular distribution and have nothing useful to offer an outlier. That is the "comfort zone" failure the paper worries about in its introduction, made visible.

Bandwidth is a runtime knob, not a retrain

Now the property that made EnCodec practical. "By selecting a variable number of residual steps at train time, a single model can be used to support multiple bandwidth target."

Because the stages are strictly ordered — stage c only ever sees what stages 1 through c−1 left behind — you can simply stop early. Transmit the first 8 indices instead of all 32 and the decoder reconstructs from 8; quality degrades gracefully rather than catastrophically. The bitstream is nested: the 1.5 kbps stream is a literal prefix of the 24 kbps stream.

The paper trains for this explicitly: "When doing variable bandwidth training, we select randomly a number of codebooks as a multiple of 4, i.e. corresponding to a bandwidth 1.5, 3, 6, 12 or 24 kbps at 24 kHz." Configuration: at most 32 codebooks (16 for the 48 kHz models) with 1024 entries each, "e.g. 10 bits per codebook."

A small inconsistency in the paper, worth catching. The listed bandwidths map to Nq = 2, 4, 8, 16, 32 — and 2 is not a multiple of 4. Either the 1.5 kbps point is an exception to the "multiple of 4" sampling rule, or the rule is really "a power of two times 2". The released implementation samples from the explicit bandwidth list. This does not change any result; it is included here because reading a paper well means noticing where the prose and the numbers disagree slightly, rather than smoothing it over.

The 48 kHz model supports 3, 6, 12 and 24 kbps. Check the arithmetic there too: 150 frames/s × 10 bits × Nq = 1500 · Nq bits/s, so Nq = 2, 4, 8, 16 — and 16 is stated as the 48 kHz maximum. Everything closes.

Sim 7 — The bandwidth ladder: one model, five bitrates

Drag the bandwidth slider. The top strip is the nested bitstream — watch how the lower-rate streams are literal prefixes of the higher ones. The middle panel reconstructs a toy signal with the first Nq stages so you can see detail arriving. The bottom curve is measured residual energy versus stage, with the current operating point marked, plus the MUSHRA score the paper reports at that rate on music.

Bandwidth 6 kbps

RVQ in code, three ways

Encode, exactly the loop from the derivation:

python — RVQ encode, step by step
import numpy as np

def rvq_encode(z, codebooks):
    """z: [D]. codebooks: list of [N, D]. returns list of int indices."""
    residual, indices = z.copy(), []
    for C in codebooks:
        d2 = ((C - residual)**2).sum(axis=1)     # [N]
        k  = int(d2.argmin())
        indices.append(k)
        residual = residual - C[k]              # the ONLY state carried forward
    return indices, residual                    # residual == reconstruction error

def rvq_decode(indices, codebooks):
    return sum(C[k] for k, C in zip(indices, codebooks))

# reproduce the hand-worked example exactly
C1 = np.array([[1.,0.], [0.,1.], [-1.,0.], [0.,-1.]])
C2 = np.array([[.2,.1], [-.1,-.3], [0.,.4], [-.3,.05]])
idx, res = rvq_encode(np.array([0.9, -0.4]), [C1, C2])
print(idx, res, np.linalg.norm(res))
# [0, 1] [ 0.  -0.1] 0.1     <- matches the hand derivation
print(rvq_decode(idx, [C1, C2]))   # [0.9 -0.3]

The training-time forward pass, with straight-through and the commitment loss assembled — note that only two lines differ from inference:

pytorch — RVQ forward with straight-through and commitment
def rvq_forward(z, codebooks, n_q):
    """z: [B, D, T]. Returns quantized z_q, index tensor, commitment loss."""
    residual = z
    z_q      = torch.zeros_like(z)
    commit   = z.new_zeros(())
    idxs     = []
    for c in range(n_q):                       # n_q chosen per batch: 2,4,8,16,32
        C   = codebooks[c]                        # [N, D]
        flat = residual.permute(0,2,1).reshape(-1, C.shape[-1])
        d2  = (flat.pow(2).sum(-1, keepdim=True)
               - 2 * flat @ C.T
               + C.pow(2).sum(-1))
        k   = d2.argmin(-1)
        q   = C[k].view_as(flat).reshape(residual.permute(0,2,1).shape).permute(0,2,1)
        # Eq. 3: MSE between the residual and its quantized value,
        # gradient only w.r.t. the residual -> detach the codebook side
        commit = commit + torch.nn.functional.mse_loss(residual, q.detach())
        z_q      = z_q + q
        residual = residual - q.detach()          # codebooks learn by EMA, not by grad
        idxs.append(k)
    z_q = z + (z_q - z).detach()                  # straight-through to the encoder
    return z_q, torch.stack(idxs, 1), commit

And the library one-liner, because in practice nobody writes the loop:

python — the shipped API
from encodec import EncodecModel
model = EncodecModel.encodec_model_24khz()
model.set_target_bandwidth(6.0)          # 1.5 | 3 | 6 | 12 | 24 -> picks n_q
frames = model.encode(wav)                 # [(codes [B, n_q, T], scale)]
wav_hat = model.decode(frames)
Read the shapes in that last snippet against the loop above. codes is [B, n_q, T] of integers in [0, 1023]. Setting the target bandwidth does not reload weights or change the graph — it only changes how many iterations the loop runs. One trained model, five operating points, chosen at call time. That is a systems property, not a modelling one, and it is why EnCodec was so easy to drop into other people’s pipelines.

What happens between the quantizer and the decoder

One sentence in the paper is easy to skim past and it matters: "This discrete representation can changed again to a vector by summing the corresponding codebook entries, which is done just before going into the decoder."

So the decoder does not receive Nq separate streams, and it does not receive integers. It receives a single [B, D, T] float tensor that is the sum of the selected entries. The decoder cannot tell whether that tensor came from 2 codebooks or 32 — it only sees a vector that is closer or further from what the encoder produced. This is why a single decoder handles all five bitrates without any conditioning: the bitrate manifests purely as how accurate the input is.

In the hand-worked example, stage 1 leaves residual (−0.1, −0.4) and stage 2 selects C2[1] = (−0.1, −0.3). What is the final reconstruction error, and why does it equal the stage-3 residual?
Why can one trained EnCodec serve 1.5, 3, 6, 12 and 24 kbps without retraining or any bitrate conditioning in the decoder?

Chapter 5: The Loss Anatomy

You now have an encoder that compresses, a quantizer that discretises, and a decoder that reconstructs. Nothing so far explains why the output sounds good. That is entirely the training objective’s job, and EnCodec’s objective has four terms plus a commitment term, each answering a different failure mode.

Start with the failure modes, so each term is demanded before it appears.

If you train with only…What you getWhy
Time-domain L1 or L2Dull, muffled audio. Excellent SI-SNR.Waveform distance is dominated by high-energy low frequencies; phase errors of a fraction of a millisecond look catastrophic to it while being inaudible.
Spectrogram distanceCorrect spectral envelope, but buzzy / metallic.Magnitude spectrograms discard phase. Many phase assignments give the same magnitude and most of them sound wrong.
Adversarial loss onlyPlausible audio that is not this audio.A discriminator rewards realism, not fidelity. Nothing anchors the output to the input.

So you need all three, and the paper uses exactly all three plus feature matching. Here is Equation 4, the generator objective, with every symbol defined:

LG = λt · ℓt(x, x̂) + λf · ℓf(x, x̂) + λg · ℓg(x̂) + λfeat · ℓfeat(x, x̂) + λw · ℓw(w)
SymbolNameWhat it measuresλ at 24 kHz
tTime-domain lossL1 between waveforms0.1
fFrequency-domain lossMulti-scale mel-spectrogram L1 + L21
gGenerator adversarial lossHinge loss against the discriminators3
featRelative feature matchingDistance between discriminator internal activations3
wVQ commitment lossEncoder output vs its quantized valuesee Ch 6 — it sits outside the balancer

At 48 kHz the paper changes only two: λg = 4 and λfeat = 4.

Term 1: the time-domain loss

t(x, x̂) = ‖x − x̂‖1

Plain L1 between waveforms. L1 rather than L2 because L1’s gradient has constant magnitude, so quiet passages get the same corrective pressure as loud ones — L2 would let the model ignore anything below the noise floor of the loudest section. Its weight is the smallest in the objective (0.1), which tells you the authors regard it as an anchor rather than a driver.

Term 2: the multi-scale mel loss, unpacked symbol by symbol

This is Equation 1, and it looks worse than it is:

f(x, x̂) = (1 / (|α| · |s|)) · ∑αi ∈ αi ∈ e [ ‖Si(x) − Si(x̂)‖1 + αi ‖Si(x) − Si(x̂)‖2 ]
SymbolMeaningConcrete value in EnCodec
SiA 64-bin mel-spectrogram computed with a normalized STFTwindow size 2i, hop length 2i/4
eThe set of scales, i.e. the exponentse = 5, …, 11 → seven scales
αScalars balancing the L1 and L2 terms"we take αi = 1"
|α| · |s|Normalisation so the loss does not grow with the number of scalesthe count of (α, scale) pairs

Write out the seven scales explicitly, because "multi-scale" stays abstract until you do:

iWindow = 2iHop = 2i/4Window duration at 24 kHzSees
53281.33 mstransients, click onsets — near-perfect time resolution, almost no frequency resolution
664162.67 msplosives, drum attacks
7128325.33 msfast formant motion
82566410.7 msthe classic speech-analysis window
951212821.3 mspitch and harmonic structure
10102425642.7 mstimbre, low-frequency detail
11204851285.3 msbass notes, room tone, sustained resonance
Why seven scales instead of one good one. The time–frequency uncertainty principle: a short window resolves when something happened but not what pitch it was; a long window resolves pitch but smears the onset. There is no single window that is right for both a hi-hat and a bass note. Rather than choose, the loss demands agreement at all seven resolutions simultaneously. A model that fakes a transient will pass the 2048 window and fail the 32; a model that gets the pitch wrong will pass the 32 and fail the 2048. The intersection of seven constraints is much tighter than any one of them.

And why mel bins rather than linear frequency bins? Because the mel scale is roughly logarithmic above 1 kHz, matching how human frequency discrimination degrades at high frequencies. Sixty-four mel bins put most of their resolution where hearing is sharpest. This is the one place where classical psychoacoustics survives inside EnCodec — not as a masking model, but as the choice of axis the loss is measured on.

Both L1 and L2 appear on each scale. L1 is robust and treats all bins alike; L2 punishes large individual errors hard. Using both means "get everything roughly right (L1) and never get anything badly wrong (L2)".

Sim 8 — The multi-scale mel loss: seven views of the same second

One signal — a bass note plus a sharp transient — analysed at all seven window sizes. Slide through the scales and watch the same event turn from a vertical spike (short window: precise in time, smeared in frequency) into a horizontal band (long window: precise in frequency, smeared in time). Introduce an error in the reconstruction and see which scales notice it: a timing error is invisible at i = 11 and glaring at i = 5.

Scale i i = 8

Working one mel-loss term by hand

Equation 1 is a double sum, which hides how small each piece is. Reduce it to three mel bins at one scale and compute the whole thing.

Say scale i = 8 (window 256) produces, for one time frame, the log-mel values:

S8(x) = (−2.0,  1.5,  0.4)     S8(x̂) = (−1.6,  1.9,  0.1)

The difference vector is (−2.0 − (−1.6), 1.5 − 1.9, 0.4 − 0.1) = (−0.4, −0.4, 0.3). Now the two norms:

L1:  ‖·‖1 = |−0.4| + |−0.4| + |0.3| = 0.4 + 0.4 + 0.3 = 1.1
L2:  ‖·‖2 = √(0.16 + 0.16 + 0.09) = √0.41 = 0.6403

With αi = 1 the bracket in Equation 1 is 1.1 + 1 × 0.6403 = 1.7403 for this scale.

Now the normaliser. There are |s| = 7 scales and |α| = 1 coefficient set, so the prefactor is 1 / (1 × 7) = 1/7. If all seven scales happened to give the same 1.7403, the total would be (1/7) × 7 × 1.7403 = 1.7403 — unchanged. That is the point of the normaliser: adding scales must not inflate the loss, or λf = 1 would silently mean something different for a five-scale model than a seven-scale one.

Notice the two norms disagreeing on what matters. If instead the difference had been (0, 0, 1.1) — the same L1 of 1.1 concentrated in one bin — the L2 would be 1.1 rather than 0.6403, and the bracket would jump to 2.2. Same total error, 26% higher loss, purely because it is concentrated. That is exactly the behaviour you want: a model that gets one band badly wrong is worse than one that is slightly off everywhere, because a single wrong band is an audible artifact while a diffuse error is a slight colouration.

python — Equation 1, complete
import torch, torchaudio

SCALES = range(5, 12)                 # e = 5..11 -> windows 32 .. 2048

mels = {i: torchaudio.transforms.MelSpectrogram(
            sample_rate=24000,
            n_fft=2**i,                    # window size 2^i
            hop_length=(2**i) // 4,         # hop 2^i / 4
            n_mels=64,                     # "a 64-bins mel-spectrogram"
            normalized=True)                # "a normalized STFT"
        for i in SCALES}

def multi_scale_mel_loss(x, x_hat, alphas=(1.0,)):
    total = 0.0
    for a in alphas:                    # the paper uses alpha_i = 1
        for i in SCALES:
            S, S_hat = mels[i](x), mels[i](x_hat)
            total = total + (S - S_hat).abs().mean() \
                          + a * (S - S_hat).pow(2).mean().sqrt()
    return total / (len(alphas) * len(list(SCALES)))    # the 1/(|alpha|·|s|) prefactor
The gotcha hiding in that code. A mel-spectrogram of a silent frame is all zeros, and if you take a logarithm of it you get negative infinity, which poisons the gradient of every scale at once. Real implementations clamp before the log (torch.log(S.clamp(min=1e-5))). EnCodec trains on mixtures that frequently contain near-silent passages — and it applies a random gain between −10 and +6 dB on top — so this is not a theoretical concern. It is the single most common way a reimplementation of this loss produces NaN in the first hundred steps.

Term 3: the MS-STFT discriminator

Reconstruction losses give you the right average. They do not give you realism, because averaging over the many plausible outputs produces something that is none of them — the classic blurring failure. The fix is a discriminator: a network trained to tell real audio from reconstructed audio, whose confusion becomes the generator’s reward.

EnCodec’s discriminator is the MS-STFT discriminator, and its architecture is specified precisely enough to rebuild:

PropertyValue
Number of sub-discriminators K5, one per STFT scale
STFT window lengths[2048, 1024, 512, 256, 128], each with hop = window / 4
InputThe complex-valued STFT, with real and imaginary parts concatenated as channels
First layerConv2D, kernel 3 × 8, 32 channels
Middle layersConv2D with dilation in the time dimension of 1, 2, 4 and stride 2 over the frequency axis
Final layerConv2D kernel 3 × 3, stride (1, 1) → the prediction map
Activation / normLeakyReLU, weight normalization
48 kHz variantDouble every STFT window size; train the discriminator every two batches
StereoLeft and right channels processed separately
The single most important word in that table is "complex". The reconstruction loss uses magnitude mel-spectrograms, which are phase-blind. The discriminator gets the real and imaginary parts, so it sees phase. That division of labour is the design: reconstruction losses pin down the spectral envelope, and the adversary polices the thing the reconstruction losses structurally cannot see. If you remember one architectural fact from this chapter, remember that the two loss families are looking at complementary halves of the same transform.

Note also the asymmetric dilation and stride: dilated in time (1, 2, 4 — growing the temporal receptive field cheaply, so a sub-discriminator can judge rhythm and decay) and strided in frequency (halving the frequency axis each layer, since neighbouring frequency bins are highly redundant). The discriminator is built to look along time and summarise across frequency.

The generator’s adversarial loss is a hinge, averaged over the K discriminators:

g(x̂) = (1/K) ∑k max(0, 1 − Dk(x̂))

and the discriminators minimise the corresponding hinge:

Ld(x, x̂) = (1/K) ∑k [ max(0, 1 − Dk(x)) + max(0, 1 + Dk(x̂)) ]

Read the hinge carefully. For real audio the discriminator wants Dk(x) ≥ 1; for fake audio it wants Dk(x̂) ≤ −1. Once a sample is on the correct side by a margin of 1 it contributes zero gradient. That is the hinge’s virtue over cross-entropy: it stops pushing on examples it has already won, which keeps the discriminator from running away.

It runs away anyway, sometimes. The paper: "Given that the discriminator tend to overpower easily the decoder, we update its weight with a probability of 2/3 at 24 kHz, and 0.5 at 48 kHz." A coin flip, biased, deciding whether the discriminator learns this step. Crude, effective, and honest — and note that the 48 kHz model needs the stronger handicap because a longer, higher-resolution STFT gives the discriminator more to work with.

Term 4: relative feature matching

The adversarial signal alone is a single scalar per sub-discriminator: too coarse and too unstable to train a decoder on. Feature matching adds a much richer signal by comparing the discriminator’s internal activations on real versus reconstructed audio. Equation 2:

feat(x, x̂) = (1 / (K · L)) ∑k=1Kl=1L [ ‖Dkl(x) − Dkl(x̂)‖1 / mean(‖Dkl(x)‖1) ]

where Dkl is the l-th layer of the k-th discriminator, L is the number of layers, and "the mean is computed over all dimensions."

The word doing the work is relative: the denominator. Without it, layers with naturally large activations would dominate the sum, and the effective weighting of the loss would drift as the discriminator’s activation scales change during training. Dividing by the mean magnitude of the real activations makes each layer contribute a fraction of its own scale.

You have now seen this idea twice. Relative feature matching normalises by the natural scale of each layer so the weights mean what you intended. In the next chapter the balancer normalises by the natural scale of each loss’s gradient so the weights mean what you intended. Same instinct, applied one level up. When you see an author apply the same fix at two different levels of a system, you are watching them articulate a principle, not patch a bug — and the paper flags this term as one of its own small improvements over SoundStream ("we use the relative feature loss introduced in Section 3.4").

Which discriminator actually matters? The ablation that reverses a metric

Prior work stacked discriminators: the Multi-Scale Discriminator (MSD) on raw waveforms at several resolutions, the Multi-Period Discriminator (MPD) which reshapes the waveform into 2-D by period, and a single STFT discriminator. SoundStream used MSD + Mono-STFT. EnCodec asks whether all that machinery is necessary. Table 2:

Discriminator setupSI-SNRViSQOLMUSHRA (human)
MSD + Mono-STFT (the SoundStream stack)5.994.2262.91 ±2.62
MPD only7.354.2460.7 ±2.8
MS-STFT + MPD6.554.3479.0 ±1.9
MS-STFT only (EnCodec)6.674.3577.5 ±1.8

Read the MPD row and then the MS-STFT+MPD row, and something should bother you.

MPD has the best SI-SNR in the table (7.35) and the worst MUSHRA (60.7). The waveform-fidelity metric and the human listeners point in opposite directions. Meanwhile MS-STFT has lower SI-SNR (6.67) and humans rate it 17 points higher.

This is the most transferable lesson in the paper, and it is buried in an ablation table. SI-SNR measures waveform alignment. A model can achieve excellent waveform alignment while producing audio full of perceptually obvious artifacts, and it can produce beautiful-sounding audio whose waveform is subtly misaligned. If you had tuned this system on SI-SNR you would have shipped the MPD variant and been 17 MUSHRA points worse. Every field has a metric like this. Find yours before it finds you.

The paper’s conclusion is deliberately modest: "using only a multi-scale STFT-based discriminator such as MS-STFTD, is enough to generate high quality audio. Additionally, it simplifies the model training and reduces training time. Including the MPD discriminator, adds a small gain when considering the MUSHRA score." That gain is 79.0 versus 77.5 — and the confidence intervals (±1.9 and ±1.8) overlap. They chose the simpler model. That is the right call on that evidence, and saying so is a small act of scientific hygiene.

Sim 9 — The discriminator ablation: when your metric lies to you

The four setups from Table 2, plotted on both axes at once. Toggle which metric drives the ranking and watch the ordering shuffle — MPD is first on SI-SNR and last on MUSHRA. The connecting lines make the rank inversion impossible to miss; the shaded bands are the reported 95% confidence intervals, so you can also see which differences are real.

Multi-bandwidth training has a discriminator wrinkle

One more detail, easy to miss and genuinely surprising: "We also noticed that using a dedicated discriminator per-bandwidth is beneficial to the audio quality. Thus, we select a given bandwidth for the entire batch, and evaluate and update only the corresponding discriminator."

So there is not one discriminator stack — there is one per supported bandwidth. This makes sense once you consider what the discriminator has to learn: the artifacts of a 1.5 kbps reconstruction are qualitatively different from those of a 24 kbps one. A single discriminator would have to be an expert on five different artifact distributions at once, and would end up mediocre at all of them. Splitting them costs training memory and buys quality; at inference the discriminators are discarded entirely, so it costs nothing at deployment.

The reconstruction loss uses magnitude mel-spectrograms while the MS-STFT discriminator takes the complex STFT with real and imaginary parts concatenated. Why is that split deliberate?
In Table 2, MPD-only scores the best SI-SNR (7.35) and the worst MUSHRA (60.7). What should you conclude?

Chapter 6: The Balancer — Making Loss Weights Mean Something

You have five loss terms and five weights. Set λt = 0.1, λf = 1, λg = 3, λfeat = 3 as the paper does. Now answer a simple question: what fraction of the training signal comes from the adversarial loss?

The honest answer is that you have no idea. Not because the question is ill-posed, but because λg = 3 tells you nothing without knowing the natural scale of ℓg’s gradient — and that scale changes every batch, because the discriminator is itself being trained.

This is the problem the balancer solves, and it is the one genuinely new mechanism in the paper. The abstract calls it out: "We introduce a novel loss balancer mechanism to stabilize training: the weight of a loss now defines the fraction of the overall gradient it should represent, thus decoupling the choice of this hyper-parameter from the typical scale of the loss."

Manufacturing the need: a concrete disaster

Work the naive case with real numbers so the failure is not abstract. Suppose at some batch the four gradient norms measured at the model output x̂ are:

Lossλi‖gi2λi · ‖giActual shareIntended share (λi / ∑λ)
t (time L1)0.10.020.1 × 0.02 = 0.0020.03%0.1/7.1 = 1.41%
f (mel)13.001 × 3.00 = 3.00038.5%1/7.1 = 14.08%
g (adversarial)30.403 × 0.40 = 1.20015.4%3/7.1 = 42.25%
feat (feature match)31.203 × 1.20 = 3.60046.1%3/7.1 = 42.25%
total7.17.802100%100%

Do the division for one row so the arithmetic is fully visible: the mel term’s actual share is 3.000 / 7.802 = 0.3845, i.e. 38.5%. You intended 14.1%. The adversarial term you weighted most heavilyg = 3, tied for the largest) delivers 15.4% of the signal — less than half its intended share — because its gradient happens to be small this batch. And the time-domain loss, which you meant to be a 1.4% anchor, is contributing 0.03%: effectively switched off.

Now make it worse in the way GAN training actually makes it worse. Suppose the discriminator gets sharper and ‖gg‖ jumps 10× to 4.0. Recompute:

λg‖gg‖ = 3 × 4.0 = 12.0   total = 0.002 + 3.0 + 12.0 + 3.6 = 18.602   adversarial share = 12.0/18.602 = 64.5%

Nothing about your hyperparameters changed. The opponent changed, and your loss mixture silently swung from 15% adversarial to 65% adversarial. Every other term got squeezed toward irrelevance. This is precisely the "varying scale of the gradients coming from the discriminators" the paper names as its motivation, and it is why GAN training is famously twitchy.

Sit with why the usual fixes fail. You could re-tune the lambdas — but the right values change during training. You could clip gradients — but that distorts direction as well as magnitude and needs its own threshold. You could normalise each loss by its value — but the value and the gradient norm are not proportional, and a loss can be small while its gradient is huge. The balancer’s insight is to normalise the exact quantity that matters for optimisation: the gradient norm, measured where all the losses meet.

The mechanism, derived

The construction has three moves. Follow each one and the final formula becomes inevitable rather than magical.

Move 1: measure the gradient at the model output, not at the parameters. Define, for each loss that depends only on the model output x̂:

gi = ∂ℓi / ∂x̂

Why at x̂ and not at the weights? Because all these losses meet at exactly one place — the output — and everything after that point is a single shared backward pass through the decoder. Rescale the gradients where they meet and you have rescaled their entire downstream contribution, with one backward pass instead of five. It is both the semantically right place and the cheap place.

Move 2: divide out each loss’s natural scale. Let ⟨‖gi2β be the exponential moving average of that gradient norm over recent training batches, with β = 0.999. Then gi / ⟨‖gi2β is a vector of typical norm 1, pointing in the direction that loss wants to move the output.

The EMA rather than the instantaneous norm matters: dividing by the current batch’s own norm would make every loss contribute exactly its share every step, destroying the useful information that this particular batch is unusually hard for the mel loss. β = 0.999 means a time constant of about 1000 batches — slow enough to track the trend, far too slow to react to one batch.

Move 3: reassemble with weights that are now pure proportions. Given weights (λi) and a reference norm R, define the balanced gradient:

i = R · ( λi / ∑j λj ) · ( gi / ⟨‖gi2β )

and backpropagate ∑ii instead of ∑i λi gi. The paper sets R = 1 and β = 0.999.

Now check the property that makes it worth doing. Take the norm of one balanced term:

‖g̃i‖ = R · (λi / ∑jλj) · ‖gi‖ / ⟨‖gi‖⟩β  ≈  R · λi / ∑jλj

because ‖gi‖ is, on average, equal to its own moving average. So each term contributes a gradient of norm exactly its weight share, independent of the loss’s units, its scale, or what the discriminator is doing this week. And if the lambdas sum to 1, each λi is literally the fraction of the model’s gradient coming from that loss. The paper: "If ∑i λi = 1, then each weight can be interpreted as the fraction of the model gradient that come from the corresponding loss."

Redo the disaster with the balancer on. Same four losses, same wildly different gradient norms 0.02, 3.0, 0.4, 1.2 — and now suppose each equals its own EMA. Then g̃t has norm 1 × 0.0141 = 0.0141, g̃f has norm 0.1408, g̃g has norm 0.4225, g̃feat has norm 0.4225. Exactly the intended shares, to four decimals. Then let the discriminator sharpen and ‖gg‖ jump 10×: the EMA drags up over the next thousand batches and the share returns to 42.25%. The spike is absorbed, not amplified.
Sim 10 — The balancer lab: intended shares versus what actually happens

Left column: the four lambdas, and the pie of shares you intended. Right column: the same four losses with independently adjustable gradient norms, and the pie of shares you actually get. Toggle the balancer to see the right pie snap onto the left one. Then press "discriminator spike" and watch the unbalanced mixture lurch while the balanced one holds — and the EMA trace at the bottom shows the balancer catching up over the following batches.

λ time0.1
λ mel1.0
λ adversarial3.0
λ feature3.0
‖g adversarial‖0.40

The one loss that does not fit

"All the generator losses from Eq. (4) fit into the balancer, except for the commitment loss, as it is not defined with respect to the output of the model."

This exclusion is not arbitrary and it is worth understanding, because it defines the balancer’s applicability. The commitment loss ℓw = ∑c ‖zc − qc(zc)‖22 is a function of the encoder’s output z, not the decoder’s output x̂. Its gradient does not pass through x̂ at all — it enters the graph at the quantizer and flows only backwards into the encoder.

So ∂ℓw/∂x̂ is undefined, the whole construction has nothing to normalise, and the commitment loss is simply added with a plain scalar weight in the ordinary way. The balancer is a tool for losses that share a common output node. That is a real restriction, and it is exactly why the paper states it explicitly rather than quietly.

The evidence: Table A.4

The appendix trains EnCodec (with the DiffQ quantizer, on the Jamendo music dataset) across many weight settings, with and without the balancer. A selection, with the paired rows adjacent:

λtλfλgλfeatBalancerSI-SNRViSQOL
1111yes10.324.16
1111no6.163.89
1241yes9.934.17
1241no1.723.52
121001yes8.414.05
121001no−35.832.82
1021004yes9.224.09
1021004no−16.392.95

Three things this table proves, in ascending order of importance:

1. The balancer helps even at sane settings. All-ones weights: 10.32 versus 6.16 SI-SNR. A 4 dB gap from a normalisation trick, at the setting a careful practitioner would try first.

2. The balancer converts catastrophes into inconveniences. At λg = 100 — an absurd setting, deliberately — the unbalanced run reaches −35.83 dB SI-SNR. Negative SI-SNR means the output is further from the target than silence would be: the model has diverged completely. The same setting with the balancer gives 8.41 dB and ViSQOL 4.05, which is a perfectly usable model.

3. The balanced column is nearly flat. Read down the "yes" rows: 10.32, 9.93, 8.41, 9.22. Across weight settings spanning two orders of magnitude, quality varies by under 2 dB. Read down the "no" rows: 6.16, 1.72, −35.83, −16.39. The balancer has turned a knife-edge hyperparameter into a plateau.

The real contribution is not quality — it is the search. Look again at reading 3. Without the balancer, finding good lambdas requires a search over a landscape where a factor-of-25 error in one coordinate destroys the run. With it, everything in a wide region works. The paper puts it plainly: "Following the balancer approach significantly reduce the effort needed for tuning the objective coefficients" and "we demonstrate that the use of the balancer shows no degradation compared to an identified combination of coefficients." That last clause is the important one — the balancer is not merely a safety net that costs you peak performance. It matches the best hand-tuned result and removes the tuning.

The balancer in code

pytorch — the balancer, complete
class Balancer:
    """weights: {name: lambda}.  R = reference norm.  beta = EMA decay."""
    def __init__(self, weights, R=1.0, beta=0.999):
        self.w, self.R, self.beta = weights, R, beta
        self.avg, self.count = {}, 0

    def backward(self, losses, x_hat):
        # 1. gradient of each loss w.r.t. the MODEL OUTPUT (not the params)
        grads = {}
        for name, loss in losses.items():
            g, = torch.autograd.grad(loss, [x_hat], retain_graph=True)
            grads[name] = g

        # 2. exponential moving average of each gradient NORM
        self.count += 1
        norms = {}
        for name, g in grads.items():
            n = g.norm(p=2).item()
            prev = self.avg.get(name, 0.0)
            self.avg[name] = self.beta * prev + (1 - self.beta) * n
            # bias correction, exactly as in Adam: early estimates are shrunk
            norms[name] = self.avg[name] / (1 - self.beta ** self.count)

        # 3. reassemble: each term gets norm R * lambda_i / sum(lambda)
        total_w = sum(self.w[k] for k in grads)
        out = torch.zeros_like(x_hat)
        for name, g in grads.items():
            scale = self.R * self.w[name] / total_w / (norms[name] + 1e-12)
            out = out + scale * g

        # 4. ONE backward pass through the decoder with the balanced gradient
        x_hat.backward(out)

# usage: the commitment loss is NOT passed in -- it does not depend on x_hat
balancer = Balancer({'t': 0.1, 'f': 1.0, 'g': 3.0, 'feat': 3.0})
balancer.backward({'t': l_t, 'f': l_f, 'g': l_g, 'feat': l_feat}, x_hat)
(lambda_w * l_commit).backward()          # added the ordinary way
optimizer.step()

Note the shape of step 4: five losses, but only one backward pass through the decoder and encoder. The per-loss autograd.grad calls stop at x̂, which is a tiny fraction of the graph. The balancer is close to free.

Honest caveat the paper states. "This changes the optimization problem" — the balanced update is not the gradient of any fixed objective, because the scaling factors depend on the running gradient statistics. You have given up the guarantee that you are descending a well-defined loss surface, in exchange for interpretable weights and stable training. In practice this is the same bargain Adam and batch-norm already make, and nobody loses sleep over it. But it is a real bargain, and pretending otherwise would be sloppy.

Where else this idea belongs

Any multi-task system where several losses meet at one tensor is a candidate: multi-task detection heads, VAE reconstruction versus KL, physics-informed networks with wildly different residual scales, distillation losses mixed with task losses. The signature symptom is "I retuned the weights and the model got worse in a way I cannot explain." That symptom means your weights are entangled with scales, and the balancer — or the same idea rebuilt for your graph — disentangles them.

With λt=0.1, λf=1, λg=3, λfeat=3 and gradient norms 0.02, 3.0, 0.4, 1.2, what fraction of the naive gradient comes from the mel loss, and what does the balancer make it?
Why is the commitment loss excluded from the balancer?
Table A.4 shows SI-SNR of −35.83 without the balancer at λg=100, and 8.41 with it. What does a negative SI-SNR tell you?

Chapter 7: Entropy Coding — Getting Bits Back for Free

Chapter 0 flagged a weakness and promised to fix it here. EnCodec’s bitrate is constant: every frame costs exactly Nq × 10 bits whether it contains a cymbal crash or a held organ note. That is wasteful, because the code indices are not uniformly distributed and they are not independent across time.

The fix is the last stage of the classical pipeline that we have not yet used: lossless entropy coding. And this time the probability model is a small Transformer.

The information-theoretic floor, worked by hand

Shannon’s result is the whole justification, so make it concrete before generalising. A symbol you assign probability p costs log2(1/p) bits to encode optimally. Rare symbols are expensive; predictable symbols are nearly free. The expected cost per symbol is the entropy:

H(p) = − ∑k pk log2 pk

Worked example. Take a toy codebook with four entries. If you know nothing, the distribution is uniform, p = (0.25, 0.25, 0.25, 0.25), and:

H = −4 × 0.25 × log2(0.25) = −4 × 0.25 × (−2) = 2 bits

which is just log2(4) — the fixed-rate cost. Now suppose a language model looks at the previous frame and predicts p = (0.60, 0.20, 0.15, 0.05). Compute every term:

−0.60 · log2(0.60) = −0.60 × (−0.73697) = 0.44218
−0.20 · log2(0.20) = −0.20 × (−2.32193) = 0.46439
−0.15 · log2(0.15) = −0.15 × (−2.73697) = 0.41055
−0.05 · log2(0.05) = −0.05 × (−4.32193) = 0.21610
H = 0.44218 + 0.46439 + 0.41055 + 0.21610 = 1.53322 bits

So the same symbol stream now costs 1.533 bits instead of 2. The saving:

(2 − 1.53322) / 2 = 0.23339 = 23.3% fewer bits, for zero quality loss

Scale that intuition to EnCodec: 1024 entries, uniform cost 10 bits. If a Transformer that has seen the previous frames can concentrate its prediction enough to reach an entropy of about 6.5 bits, you have saved 35%. The paper reports savings of "∼25–40%", so that is roughly the regime it operates in.

Notice that nothing is thrown away. Entropy coding is exactly lossless. The decoder reconstructs the identical integer indices, so the audio is bit-identical to the non-entropy-coded version. This is a rare thing in a paper full of trade-offs: a strict improvement in bitrate at zero quality cost. What it does cost is time — and Table 5 will show that cost is severe.

The language model, specified exactly

"We additionally train a small Transformer based language model with the objective of keeping faster than real time end-to-end compression/decompression on a single CPU core."

PropertyValueWhy it is that small
Layers5The entire design constraint is a single CPU core in real time. A big model would predict better and lose the RTF budget. This is a systems paper making a systems choice.
Attention heads8
Model width200 channels
Feed-forward dimension800
DropoutnoneThe dataset is effectively unbounded — every second of training audio is new code sequences
Causal receptive field3.5 seconds≈ 262 frames at 75 Hz; audio structure beyond a few seconds is not predictive of individual codes
Training sequence length5 secondsLonger than the receptive field, so every position sees full context
Position embeddingsSinusoidal, with a random initial offset"to emulate being in a longer sequence" — the model must work at any absolute position in an arbitrarily long stream, not only near position zero

For scale: 5 layers × 200 channels is on the order of a few million parameters. GPT-2 small is 124 million. This is a probability model, not a generator, and the paper is explicit that its smallness limits it — a point that returns at the end of this chapter.

The input and output shapes, traced

The mechanics are unusual enough to walk carefully.

Input. "At train time, we select a bandwidth and the corresponding number of codebooks Nq. For a time step t, the discrete representation obtained at time t − 1 is transformed into a continuous representation using learnt embedding tables, one for each codebook, and which are summed. For t = 0, a special token is used instead."

codes at t−1
Nq integers, each in [0, 1023] — the previous frame’s full RVQ stack
↓ Nq separate embedding tables, each 1024 × 200
summed embedding
One 200-dimensional vector. Summing (not concatenating) keeps the width fixed at 200 no matter whether Nq is 2 or 32 — the same Transformer serves every bandwidth.
↓ 5 causal Transformer layers, 8 heads, 3.5 s receptive field
Nq linear heads
Each maps 200 → 1024 logits: the predicted distribution over codebook c’s entry at time t. All Nq heads read the same hidden state.

That last box hides the paper’s most consequential approximation, and it states it in one sentence: "We thus neglect potential mutual information between the codebooks at a single time step."

Unpack it. In RVQ, codebook 2’s index at time t depends heavily on codebook 1’s index at time t — stage 2 is quantizing stage 1’s residual, so knowing stage 1’s choice tells you a lot about stage 2’s. A model that predicted them sequentially could exploit that. This model predicts all Nq in parallel from the same hidden state, conditioning only on the past, so that within-timestep information is thrown away.

Why throw away real information on purpose? Speed. The alternative — "having one time step per codebook, or a multi-stage prediction" — multiplies the number of sequential Transformer calls by Nq. At Nq = 32 that is 32× the latency, and the whole point of this component is to stay faster than real time on one CPU core. The paper accepts the loss explicitly: this "allows to speedup inference … with a limited impact over the final cross entropy." Notice the shape of that sentence: a quantified cost, a named benefit, an explicit judgement. That is what a well-made engineering trade-off looks like written down.

Arithmetic coding: turning probabilities into bits

A probability model alone saves nothing — you need a coder that can actually spend a fractional number of bits on a symbol. Huffman codes cannot: they assign whole bits, so a symbol with p = 0.9 (ideal cost 0.152 bits) still costs at least 1. Arithmetic coding can, and EnCodec uses a range-based arithmetic coder (Pasco 1976; Rissanen & Langdon 1981).

The idea in one picture: represent the entire message as a single number in [0, 1). Start with the full interval. For each symbol, subdivide the current interval in proportion to the predicted probabilities and keep the sub-interval belonging to the symbol that actually occurred. After many symbols the interval is tiny, and the number of bits needed to name a point inside it is exactly the sum of the log2(1/p) costs.

Trace it by hand with the toy distribution p = (0.60, 0.20, 0.15, 0.05) and the message [0, 2]:

Start: [0.00000, 1.00000), width 1.00000
Symbol 0 owns the first 60%: new interval [0.00000, 0.60000), width 0.60000
Within it, symbol 2 owns the slice from 80% to 95%:
low = 0.00000 + 0.60000 × 0.80 = 0.48000   high = 0.00000 + 0.60000 × 0.95 = 0.57000
Final interval [0.48000, 0.57000), width 0.09000

Bits required to name a point in an interval of width w is log2(1/w):

log2(1 / 0.09) = log2(11.111) = 3.474 bits

Check it against the per-symbol costs: log2(1/0.60) + log2(1/0.15) = 0.73697 + 2.73697 = 3.47394. Identical. Fixed-rate coding would have spent 2 + 2 = 4 bits. The saving is real and it is fractional — 3.474, not 3 or 4.

Sim 11 — The arithmetic coder: watch the interval close

Each row is one symbol. The bar is the current interval, subdivided by the model’s predicted probabilities; the highlighted slice is the symbol that actually occurred and becomes the next row’s full width. The running bit cost is log2 of the reciprocal interval width. Sharpen the model’s predictions and watch the same message get cheaper — flatten them toward uniform and the cost climbs back to the fixed rate.

Model sharpness 0.65

The floating-point trap, and how the paper defuses it

Here is a detail most papers would omit and this one spends a paragraph on, because it is the difference between a demo and a deployable codec.

An arithmetic decoder must reconstruct the exact same interval subdivisions the encoder used. That means the decoder’s language model must output bit-identical probabilities to the encoder’s. And it does not, in general.

The paper: "evaluation of the same model might lead to different results on different architectures, or with different evaluation procedures due to floating point approximations. This can lead to decoding errors as the encoder and decoder will not use the exact same code. We observe in particular that the difference between batch evaluation (e.g. all time steps at once), and the real-life streaming evaluation that occurs in the decoder can lead to difference larger than 10−8."

Read that carefully: the same model, the same weights, the same input — but evaluated all-at-once versus one-step-at-a-time, and the answers differ in the eighth decimal place because floating-point addition is not associative and the reduction orders differ. In an arithmetic coder, a discrepancy of 10−8 in a probability can flip which sub-interval a value falls into, and every subsequent symbol decodes to garbage.

DefenceValueEffect
Round the estimated probabilitiesprecision 10−6Two orders of magnitude coarser than the observed 10−8 discrepancy, so both sides round to the same value
Total range width224Integer arithmetic, not floats, inside the coder itself — exactly reproducible
Minimum range width2No symbol can ever be assigned zero width, which would make it un-encodable

And the honest hedge, in the paper’s own words: "although evaluations in more contexts would be needed for practical deployment." They are telling you this is mitigated, not solved. Cross-platform bit-exactness of neural network inference is an unsolved problem, and any codec whose correctness depends on it inherits that problem.

The general principle worth extracting. Any system where two parties must independently recompute the same neural network output and get identical results is fragile. Arithmetic coding with a learnt model is the classic instance, but the same trap appears in distributed training with lossy all-reduce, in verifiable inference, and in any protocol where a hash of model outputs is compared. The standard defences are the two used here: quantize the shared quantity coarsely enough to absorb the drift, and do the protocol arithmetic in integers.

What it buys, and what it costs

The bandwidth savings, from Table 1 (24 kHz mono) and Table 4 (48 kHz stereo). The "entropy coded" column is the average bandwidth after coding — it is now variable, so only an average is meaningful:

NominalAfter entropy codingSavingSetting
1.5 kbps0.9 kbps40.0%24 kHz mono
3.0 kbps1.9 kbps36.7%24 kHz mono
6.0 kbps4.1 kbps31.7%24 kHz mono
12.0 kbps8.9 kbps25.8%24 kHz mono
6.0 kbps4.2 kbps30.0%48 kHz stereo
12.0 kbps8.9 kbps25.8%48 kHz stereo
24.0 kbps19.4 kbps19.2%48 kHz stereo

The trend is monotone and the paper explains it: "We observe that for higher bandwidth, the compression ratio is lower, which could be explained by the small size of the Transformer model used, making hard to model all codebooks together." Forty percent at 1.5 kbps (2 codebooks) down to 19% at 24 kbps (32 codebooks, or 16 at 48 kHz). With more codebooks per step, and no within-timestep conditioning, a 5-layer model simply runs out of capacity.

Now the bill, from Table 5. Real-time factor is audio duration over processing time, so greater than 1 means faster than real time. All at 6 kbps, single thread of a 2019 MacBook Pro:

ModelLatencyEnc.Dec.Enc. + ECDec. + EC
Lyra v2 (32 kHz)27.467.2
EnCodec 24 kHz13 ms9.810.41.61.6
EnCodec 48 kHz1 s6.85.10.680.66

At 24 kHz, entropy coding drops the real-time factor from about 10 to 1.6 — a 6× slowdown — still faster than real time, but with almost no headroom. At 48 kHz it drops below 1: the system is now slower than real time and cannot be used for live streaming at all. The paper is direct: it "could also be used for archiving where real time processing is not required."

And latency: "using entropy coding increases the initial latency, because the stream cannot be 'flushed' with each frame, in order to keep the overhead small. Thus decoding the frame at time t, requires for the frame t + 1 to be partially received, increasing the latency by 13 ms." So the 24 kHz streaming latency roughly doubles, from 13.3 ms to about 26 ms.

Sim 12 — The entropy-coding trade: bits saved versus speed lost

Both axes of Table 1 and Table 5 on one plot. Each point is an operating configuration; the horizontal axis is effective bandwidth and the vertical axis is real-time factor on a log scale, with the RTF = 1 line drawn in red. Toggle entropy coding and watch every point slide left (cheaper) and down (slower). Anything below the red line cannot be used live.

The strategic read. Entropy coding is optional in EnCodec, and that optionality is the design. For a video call you turn it off: 13 ms latency, RTF 10, plenty of headroom on a busy phone. For music streaming or archiving you turn it on: 30–40% fewer bytes stored and served, and nobody notices 26 ms. The same trained codec serves both, and no retraining separates them. A worse paper would have baked entropy coding in and reported only the bitrate win.
A Transformer predicts p = (0.6, 0.2, 0.15, 0.05) over a 4-entry codebook. What is the expected cost per symbol, and how does that compare to the fixed rate?
Savings fall from 40% at 1.5 kbps to 19.2% at 24 kbps stereo. Why?
Why does the paper round the language model’s probabilities to a precision of 10−6 before arithmetic coding?

Chapter 8: Data, Training, and the Verdict

Everything so far has been mechanism. This chapter is evidence: what the model was trained on, how, and what happened when humans listened.

The training distribution is a design decision

Chapter 1 argued that a learned transform wins inside its distribution and can fail outside it. So the choice of distribution is part of the architecture, and the paper treats it that way. The 24 kHz monophonic model is trained across four domains:

DomainDatasetsRole
SpeechClean segments from DNS Challenge 4; Common VoiceThe bread-and-butter case, and the one where classical codecs are strongest
General audioAudioSet; FSD50KEnvironmental sound, sound events — the long tail that breaks psychoacoustic models
MusicMTG-Jamendo (train and eval); a proprietary music set (eval only)The hardest case at low bitrate, and where EnCodec’s margin is largest
Noisy / reverberant speechCreated on the fly — see the mixing strategy belowRealistic call conditions rather than studio conditions

The 48 kHz fullband stereo model, by contrast, is trained on only 48 kHz music. Different product, different distribution.

The mixing strategy is where the paper manufactures the diversity it needs. Four strategies, sampled with these exact probabilities:

StrategyWhat it doesProbability
s1Sample a single source from Jamendo (music alone)0.32
s2Sample a single source from the other datasets0.32
s3Mix two sources from all datasets0.24
s4Mix three sources from all datasets except music0.12

They sum to 1.00, as they must. Note the asymmetry in s4: three-way mixes exclude music. Three simultaneous pieces of music are not a signal anyone needs to encode and would only teach the model to handle spectral chaos at the expense of realistic cases.

On top of the mixing, four augmentations:

The data splits are the quiet integrity check. For Jamendo the paper takes "96% of the artists and their corresponding tracks for train, 2% for valid and 2% for test, hence there is no artists overlap in the different sets." Splitting by artist rather than by track is the difference between a real evaluation and a fraudulent one: split by track and the model can memorise an artist’s production style, mixing chain and instrument set, then be tested on it. Whenever you read a music-ML paper, look for this sentence. Most do not have it.

Training configuration, complete

SettingValue
Epochs300
Updates per epoch2,000 → 600,000 total updates
OptimizerAdam, β1 = 0.5, β2 = 0.9
Learning rate3 × 10−4
Batch64 examples of 1 second each
Hardware8 × A100 GPUs
Balancer weights (24 kHz)λt = 0.1, λf = 1, λg = 3, λfeat = 3
Balancer weights (48 kHz)same, but λg = 4, λfeat = 4

Two of these deserve comment. β1 = 0.5 is the GAN convention, not Adam’s default of 0.9: with an opponent that changes every step, a long momentum window averages over a moving target and slows adaptation. And 1-second examples is short — but recall that the encoder’s temporal receptive field is a few hundred milliseconds and the LSTM operates over only 75 steps in that second. Longer clips would buy little and cost memory that the batch size uses better.

Total audio seen: 600,000 updates × 64 examples × 1 second = 38.4 million seconds ≈ 10,667 hours, or about 445 days of audio. Against the dataset table’s totals — 9,096 h and 2,425 h of speech, 4,989 h and 108 h of general audio, 919 h of music — that is on the order of half an epoch over the full corpus, so the model is closer to single-pass than to heavily repeated exposure. That is why dropout is unnecessary anywhere in the system.

The two objective metrics, defined

Every ablation in this paper is reported in SI-SNR and ViSQOL, and Chapter 5 showed one of them ranking systems backwards. You cannot read that result properly without knowing what each one measures, so define both.

SI-SNR is the scale-invariant signal-to-noise ratio. Given a target x and an estimate x̂, first project the estimate onto the target to remove any overall gain difference:

xtarget = ( ⟨x̂, x⟩ / ‖x‖2 ) · x     enoise = x̂ − xtarget
SI-SNR = 10 · log10 ( ‖xtarget2 / ‖enoise2 )  dB

Work one number to make the scale concrete. Suppose the residual energy is 1% of the target energy. Then the ratio is 100 and SI-SNR = 10 · log10(100) = 20 dB. EnCodec’s 6.67 dB corresponds to a ratio of 100.667 = 4.64, i.e. the error carries about 21.5% of the target’s energy.

Twenty-one percent error energy, and listeners rate it 92.9 out of 100 on music. That single juxtaposition is the argument against waveform metrics for generative codecs: the "error" is largely a phase and fine-structure difference that the ear does not encode, and SI-SNR cannot tell that apart from an audible artifact of the same energy.

ViSQOL (Virtual Speech Quality Objective Listener) is the opposite kind of tool: it is a model fitted to predict MUSHRA-like scores. It compares gammatone spectrograms of reference and test using a similarity measure, then maps that similarity through a regression trained on human ratings, producing a Mean Opinion Score on a 1–5 scale. The paper computes it with Google’s open-source implementation using the recommended recipes.

SI-SNRViSQOLMUSHRA
What it isWaveform energy ratioRegression fitted to human ratingsActual humans
Range in this paper1.89 to 7.46 dB2.60 to 4.39 (of 5)17.7 to 97.1 (of 100)
Cost per evaluationmicrosecondssecondsweeks and money
Sees phase?Yes — and over-weights itPartlyAs humans do
Trustworthy for…Detecting gross breakageRanking similar systemsEverything, but you cannot afford it often
The working discipline this implies. Use SI-SNR as a smoke alarm, not a scoreboard: a big drop means something broke, a small difference means nothing. Use ViSQOL to rank candidates during development. Spend MUSHRA on the handful of finalists. The paper follows exactly this ladder — objective metrics for the architecture ablations in Table A.3, MUSHRA for the discriminator choice in Table 2 and the headline comparisons in Tables 1 and 4 — and Table 2 exists precisely to show what happens when you skip the last rung.

The headline table

Table 1, in full, streamable setting, 24 kHz. Mean MUSHRA with 95% confidence intervals:

ModelBandwidthEntropy codedClean speechNoisy speechMusic set 1Music set 2
Reference95.5 ±1.693.9 ±1.893.2 ±2.597.1 ±1.3
Opus6.0 kbps30.1 ±2.819.1 ±5.920.6 ±5.817.9 ±5.3
Opus12.0 kbps76.5 ±2.361.9 ±2.177.8 ±3.265.4 ±2.7
EVS9.6 kbps84.4 ±2.580.0 ±2.489.9 ±2.387.7 ±2.3
Lyra-v23.0 kbps53.1 ±1.952.0 ±4.769.3 ±3.342.3 ±3.5
Lyra-v26.0 kbps66.2 ±2.959.9 ±3.375.7 ±2.648.6 ±2.1
EnCodec1.5 kbps0.9 kbps49.2 ±2.441.3 ±3.668.2 ±2.266.5 ±2.3
EnCodec3.0 kbps1.9 kbps67.0 ±1.562.5 ±2.389.6 ±3.187.8 ±2.9
EnCodec6.0 kbps4.1 kbps83.1 ±2.769.4 ±2.392.9 ±1.891.3 ±2.1
EnCodec12.0 kbps8.9 kbps90.6 ±2.680.1 ±2.591.8 ±2.592.9 ±1.2

Four comparisons worth extracting by hand, each of which the paper makes in one clause:

1. EnCodec at 3 kbps beats Lyra-v2 at 6 kbps and Opus at 12 kbps on average. Check it: 67.0 / 62.5 / 89.6 / 87.8 averages to 76.7. Lyra at 6 kbps: 66.2 / 59.9 / 75.7 / 48.6 averages to 62.6. Opus at 12: 76.5 / 61.9 / 77.8 / 65.4 averages to 70.4. EnCodec wins at a quarter of Opus’s bitrate. This is exactly the paper’s claim: "EnCodec at 3kbps reaches better performance on average than Lyra-v2 using 6kbps and Opus at 12kbps."

2. On music at 6 kbps, EnCodec is within the reference’s confidence interval. Music set 1: reference 93.2 ±2.5, EnCodec 92.9 ±1.8. The intervals overlap heavily. Listeners could barely tell 6 kbps from uncompressed on that material.

3. Noisy speech is the hard case, for everyone. Every model drops in that column. EnCodec at 12 kbps reaches 80.1, exactly matching EVS at 9.6 kbps (80.0). This is where EnCodec’s advantage is smallest, and it makes sense: noise is by construction the least predictable content, so a learned prior has the least to offer.

4. Going from 6 to 12 kbps barely helps on music. Music set 1: 92.9 → 91.8 — within noise, and nominally down. The codec has saturated on that material at 6 kbps; extra codebooks refine a residual listeners cannot hear. Clean speech, by contrast, still gains 7.5 points (83.1 → 90.6). Different content saturates at different rates, which is precisely the argument for a runtime bandwidth knob.

Sim 13 — The results explorer: every codec, every category

Table 1 made interactive. Pick a category to see MUSHRA versus bitrate for all five systems, with the reference line and 95% confidence bands drawn. Toggle "entropy-coded bitrate" to shift the EnCodec points left onto their real average bandwidth — that is the honest x-position when the language model is in use. Tap any point for the exact figure.

Stereo: the most striking table in the paper

Table 4 evaluates 48 kHz stereo music, where the reference is 1,536 kbps and the compression ratios get absurd:

ModelBandwidthEntropy codedCompressionMUSHRA
Reference95.1 ±1.8
MP364 kbps24×82.7 ±3.2
Opus6 kbps256×17.7 ±5.9
Opus24 kbps64×82.9 ±3.7
EnCodec6 kbps4.2 kbps256×82.9 ±2.4
EnCodec12 kbps8.9 kbps128×88.0 ±2.7
EnCodec24 kbps19.4 kbps64×87.5 ±2.6

Line the three 82.x rows up. MP3 at 64 kbps: 82.7. Opus at 24 kbps: 82.9. EnCodec at 6 kbps: 82.9. Statistically indistinguishable quality at one tenth the bits of MP3 and one quarter the bits of Opus. Meanwhile Opus at the same 6 kbps scores 17.7 — a 65-point gap at equal bitrate.

And note the top of the ladder: 12 kbps scores 88.0 while 24 kbps scores 87.5. Within the confidence intervals, they are equal. The paper says it plainly: "EnCodec at 12kpbs achieve comparable performance to EnCodec at 24kbps." Doubling the bitrate buys nothing here, which means the bottleneck at 12 kbps is no longer the bit budget — it is the model.

Against the direct ancestor

Table A.2 is the fairest comparison in the paper, because the authors re-implemented SoundStream themselves (the original is not open sourced) rather than relying on a third-party port. All at 3 kbps except the classical baselines:

ModelBandwidthMUSHRA
Reference96.1 ±1.41
Opus6.0 kbps21.1 ±2.62
EVS6.0 kbps62.9 ±2.18
SoundStream (re-implemented)3.0 kbps71.8 ±1.51
EnCodec with DiffQ quantizer3.0 kbps72.3 ±1.18
EnCodec with RVQ3.0 kbps76.8 ±1.31

Two readings. First, EnCodec-with-DiffQ (72.3) and SoundStream (71.8) are statistically tied — so EnCodec’s architecture and loss changes alone do not beat SoundStream. Second, swapping DiffQ for RVQ adds 4.5 points and pulls clear. The quantizer is doing the work, which retroactively justifies spending two chapters on it.

What "we reproduced a version of SoundStream with minor improvements" costs and buys. The authors also note their re-implementation adds the relative feature loss and layer normalisation in the discriminators, "which improved the audio quality during our preliminary studies" — that is, they made their baseline stronger before comparing against it. Read that in the other direction: a weaker SoundStream would have flattered EnCodec more. Strengthening your own baseline is the single most reliable signal that a comparison is honest.

The full picture, and where it ends

Figure 3 of the paper plots MUSHRA against bitrate for EnCodec (with and without entropy coding), Lyra-v2, EVS, and Opus, on a mix of speech and music. The shape of that plot is the paper’s thesis: EnCodec’s curve sits above and to the left of every baseline at every point tested, and entropy coding shifts its points further left at unchanged height.

But note where the curve stops. At 12 kbps EnCodec reaches 90.6 on clean speech while the reference is 95.5. Five points of quality remain unclaimed, and the 12-versus-24 kbps stereo result says extra bits will not claim them. That gap is the open problem the next generation of codecs inherits, and Chapter 9 follows where they took it.

On music set 1, EnCodec scores 92.9 ±1.8 at 6 kbps and 91.8 ±2.5 at 12 kbps, while the uncompressed reference scores 93.2 ±2.5. What does that pattern mean?
The Jamendo split takes "96% of the artists and their corresponding tracks for train, 2% for valid and 2% for test." Why split by artist rather than by track?
In Table A.2, EnCodec with DiffQ scores 72.3 ±1.18 and the re-implemented SoundStream scores 71.8 ±1.51, while EnCodec with RVQ scores 76.8 ±1.31. What does this isolate?

Chapter 9: Lineage — What EnCodec Became

EnCodec was published as a codec. Within a year it was better known as something else entirely: the standard way to turn audio into tokens.

Chapter 0 planted the observation and it is time to collect on it. The quantizer’s output is a tensor of shape [B, Nq, T] containing integers in [0, 1023] at 75 steps per second. Squint at that and it is a sentence: a discrete sequence over a vocabulary of 1024, with Nq parallel streams. Every technique the language-modelling world has developed — autoregressive generation, conditioning, in-context learning, scaling laws — suddenly applies to raw audio.

The reframe that mattered more than the compression. A codec asks "how few bits can carry this sound?" A tokeniser asks "what is the discrete alphabet of sound?" They turn out to be the same question, because the optimal code is the one that captures exactly the structure that recurs — and structure that recurs is exactly what a generative model needs a vocabulary for. EnCodec answered the first question and the field took the second answer for free.

The map

Sim 14 — The lineage: where EnCodec sits

Click any node to see what it contributed and what it inherited. Time runs left to right; the vertical bands separate the classical codec line, the neural vocoder line, the neural codec line, and the audio language models built on top. The highlighted path is the one this lesson followed.

Tap a node to inspect it.

Two kinds of audio token, and why both exist

The codec line and the self-supervised-speech line produce different tokens, and understanding the difference is the entry ticket to every audio language model paper written after 2022.

Acoustic tokensSemantic tokens
Produced bySoundStream / EnCodec RVQClustering self-supervised features (w2v-BERT, HuBERT)
Trained forReconstruction — every acoustic detailPrediction of masked content — linguistic and structural content
PreserveSpeaker identity, timbre, room, prosodyPhonetic and semantic content
DiscardNothing perceptually importantAlmost all acoustic detail
Good forFaithful resynthesisLong-range coherence in generation
Bad atLong-horizon structure — too many tokens, too much detailSounding like anything at all on their own

AudioLM’s contribution was to use both: model semantic tokens first to get the content right over long spans, then generate coarse and fine acoustic tokens conditioned on them to get the sound right. Reconstruction quality and long-range coherence are different problems, so they get different token streams. Follow that thread in the AudioLM lesson linked below.

What was built on top

SystemRelationship to EnCodecThe new idea
AudioLM (2022)Uses SoundStream RVQ acoustic tokens; EnCodec is the open, better-performing siblingSemantic + acoustic token hierarchy, generated in three stages
MusicGen (2023)Built directly on EnCodec by the same labCodebook interleaving patterns — how to flatten Nq parallel streams into one sequence a single Transformer can model, trading sequence length against parallelism
AudioGen / Bark / text-to-audio systemsEnCodec as the decoder for generated tokensText conditioning over audio token streams
Mimi / Moshi (2024)A streaming codec in EnCodec’s lineageDistil a semantic token into the first RVQ level, so one codec yields both token types — and run it inside a full-duplex spoken dialogue model

Notice what MusicGen’s problem is. EnCodec hands you Nq parallel streams per timestep. A standard autoregressive Transformer wants one token at a time. Do you flatten fully (sequence length × Nq, slow but exact), predict all Nq in parallel (fast, but ignores within-timestep dependencies — the same approximation the entropy-coding Transformer in Chapter 7 makes), or interleave with a delay pattern? That question exists only because RVQ produces a stack rather than a single index, and it is now a standard design axis in audio generation.

Five misconceptions worth killing

These are the things people reliably get wrong about EnCodec after reading the abstract. Each one is a real confusion with a one-line correction.

MisconceptionCorrection
"RVQ is just a bigger codebook."It is a product code. NNq reachable points from Nq·N stored vectors — but those points are constrained to be sums from fixed sets, so it is strictly weaker than an unconstrained code of the same bit count. The encoder is trained to make that constraint harmless.
"The decoder needs to know the bitrate."It does not. It receives one [B, D, T] float tensor — the sum of the selected entries. Fewer codebooks simply means a less accurate input. No conditioning, no switching.
"The discriminator is a quality metric."It is a training signal and is discarded entirely at inference. There is one discriminator stack per supported bandwidth during training, and zero at deployment.
"The balancer is just gradient clipping."Clipping bounds a magnitude and distorts direction near the threshold. The balancer renormalises each loss to a fixed share of the total, so weights become proportions. Table A.4 shows the difference: 8.41 versus −35.83 SI-SNR at the same weights.
"Entropy coding makes it lossy in a new way."Arithmetic coding is exactly lossless. The decoded integers are identical; only the number of bits on the wire changes. What it costs is speed — RTF 9.8 down to 1.6 — and 13 ms of extra latency.

The three ideas worth stealing for something else

Most of EnCodec is audio-specific. Three parts are not, and they transfer to any domain where you are compressing or discretising a learned representation.

1 · Residual quantization as a nested code
Whenever you need a discrete bottleneck with a tunable rate, stack quantizers on residuals. You get graceful degradation, a prefix-nested bitstream, and one model serving every operating point. This is now standard in image codecs and in retrieval indexes.
2 · Adversary on the blind spot
Point the discriminator at exactly what your reconstruction loss cannot see. Here: magnitude mel loss is phase-blind, so the adversary gets complex STFT. Ask of any GAN you build: what is my reconstruction loss structurally unable to penalise, and is my discriminator actually looking at that?
3 · Weights as proportions, not scalars
Any time several losses meet at one tensor, normalise their gradients by a running estimate of their own scale. Your weights become interpretable fractions, retuning stops being superstition, and a loss whose scale drifts during training stops silently taking over.

Honest limitations

The paper is unusually candid, and its limits are worth collecting in one place:

If you were implementing this tomorrow

A build order that follows the dependency graph rather than the paper’s section order. Each step is independently testable, which is the only way a system with five losses and a GAN is ever debuggable.

StepBuildHow you know it works
1Encoder and decoder with no quantizer, trained on time-domain L1 aloneThe autoencoder reconstructs near-perfectly. If it cannot, your strides or padding are wrong — check that output length equals input length exactly.
2Add the multi-scale mel lossOutput stops sounding muffled. Verify each of the seven scales computes without NaN on silence (a zero frame with a log-mel is a classic divide-by-zero).
3Add a single VQ layerWatch codebook usage. If fewer than half the entries are ever selected, your dead-entry restart is broken — that is the failure that silently halves your bitrate.
4Extend to RVQ with Nq stagesLog the residual norm per stage. It must fall roughly geometrically. A flat stage means that codebook is not learning; a stage whose residual rises means an ordering or scaling bug.
5Randomise Nq per batchEvaluate at every supported bandwidth. Quality must degrade monotonically as you truncate.
6Add the MS-STFT discriminator and the balancer togetherAdding the adversary without the balancer is where training collapses. Table A.4 is the evidence; do not learn it the expensive way.
7Convert to causal padding, swap layer norm for weight normEncode a long file in one pass and in 320-sample chunks; the outputs must match to floating-point tolerance. If they do not, some layer is still peeking at the future.
8Train the entropy model, add the arithmetic coderRound-trip test on thousands of frames. Any single mismatch corrupts everything after it, so a partial pass is a fail.
The debugging heuristic that matters most. In a system this entangled, "the audio sounds bad" is not a diagnosis. Instrument the three quantities that localise almost every failure: codebook usage (how many entries are alive), per-stage residual norm (is RVQ actually refining), and per-loss gradient norm (is one term eating the others). Those three plots turn a mysterious GAN into an ordinary piece of software — and the third one is exactly what the balancer was built to control.

The cheat sheet

Every symbol, in one table. If you can reconstruct the paper from this, you have the lesson.

SymbolMeaningValue / shape in EnCodec
xInput audio[Ca, T] in [−1, 1]; T = d · fsr
Reconstructed audiosame shape as x
E, Q, GEncoder, quantizer, decoderthe three components
zContinuous latent[B, D, T'] where T' = T / 320
zqQuantized latent (sum of chosen entries)[B, D, T'], float
C, BBase channels, number of conv blocksC = 32, B = 4
S, KStride, kernel size of a downsamplestrides (2, 4, 5, 8); K = 2S
NqNumber of RVQ codebooks used2, 4, 8, 16, 32 (max 16 at 48 kHz)
codebook sizeEntries per codebook1024 = 10 bits
frame rateLatent steps per second75 at 24 kHz, 150 at 48 kHz
tTime-domain lossL1 between waveforms; λt = 0.1
fMulti-scale mel loss64 mel bins, windows 25…211, hop = window/4, αi = 1; λf = 1
gGenerator adversarial hinge(1/K) ∑ max(0, 1 − Dk(x̂)); λg = 3 (4 at 48 kHz)
featRelative feature matchingEq. 2, normalised per layer; λfeat = 3 (4 at 48 kHz)
wCommitment lossc ‖zc − qc(zc)‖22; outside the balancer
LdDiscriminator losshinge on both real and fake; update prob. 2/3 (0.5 at 48 kHz)
KNumber of sub-discriminators5, windows [2048, 1024, 512, 256, 128]
giGradient of loss i w.r.t. x̂the balancer’s input
iBalanced gradientR · (λi/∑λj) · gi / ⟨‖gi‖⟩β
R, βReference norm, EMA decay for the balancerR = 1, β = 0.999
EMA decay (codebook)Codebook entry update rate0.99
LMEntropy-coding Transformer5 layers, 8 heads, 200 channels, FFN 800, 3.5 s receptive field
coder settingsArithmetic coderrange width 224, min width 2, probs rounded to 10−6

Rebuild it from memory: the checklist

If you can answer these without scrolling up, you own the paper.

  1. Why is the frame rate 75 Hz, and what would change if the strides were (2, 4, 4, 8)? (Total stride 256 → 93.75 frames/s, a non-integer bit budget per second.)
  2. Given 8 codebooks of 1024 entries at 75 Hz, what is the bitrate? Do it without a calculator.
  3. Quantize (0.9, −0.4) with the two toy codebooks and report both indices and the final error.
  4. Why can one model serve five bitrates, and what does the decoder actually receive?
  5. Name the four generator loss terms and say which failure mode each prevents.
  6. Why does the discriminator see the complex STFT while the reconstruction loss sees magnitude mel?
  7. Write the balancer formula and explain why the commitment loss is excluded.
  8. Why is MPD’s best-in-table SI-SNR a warning rather than a recommendation?
  9. Why do entropy-coding savings shrink from 40% to 19% as bitrate rises?
  10. What is the difference between an acoustic token and a semantic token?

Where to go next

DirectionLessonWhy
← PrerequisiteNeural Audio CodecsThe family overview: SoundStream, EnCodec, DAC, Mimi, and where each sits
← FoundationAudio RepresentationsSTFT, mel filterbanks, and why phase is the hard part — the substrate of Chapter 5
→ NextAudioLMAudio as a language: semantic plus acoustic tokens, three-stage generation
→ AppliedMusic GenerationMusicGen’s codebook interleaving patterns, built directly on EnCodec
→ SiblingTTS ArchitecturesWhere neural vocoders came from and how codec tokens changed speech synthesis
→ ContrastCLAPThe other way to make audio machine-readable: continuous embeddings aligned with text, rather than discrete tokens
The one sentence to carry away. EnCodec is a demonstration that "compress this signal" and "find the alphabet of this signal" are the same problem — and that solving it needs three things a lone reconstruction loss cannot give you: a discretisation whose effective size grows multiplicatively (RVQ), a perceptual objective that sees what the reconstruction loss is blind to (the complex-STFT adversary), and a way to make loss weights mean what you meant (the balancer).
Why did EnCodec become more famous as an audio tokeniser than as a codec?
Acoustic tokens (EnCodec-style) versus semantic tokens (clustered self-supervised features): why do systems like AudioLM use both?