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.
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:
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.
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:
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:
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 bitrate | Bits per frame (÷ 75) | What that buys |
|---|---|---|
| 1.5 kbps | 1500 / 75 = 20 bits | 2 codebooks of 1024 entries (10 bits each) |
| 3 kbps | 3000 / 75 = 40 bits | 4 codebooks |
| 6 kbps | 6000 / 75 = 80 bits | 8 codebooks |
| 12 kbps | 12000 / 75 = 160 bits | 16 codebooks |
| 24 kbps | 24000 / 75 = 320 bits | 32 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.
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.
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 & bitrate | Clean speech | Noisy speech | Music (set 1) | Music (set 2) |
|---|---|---|---|---|
| Reference (uncompressed) | 95.5 ±1.6 | 93.9 ±1.8 | 93.2 ±2.5 | 97.1 ±1.3 |
| Opus @ 6 kbps | 30.1 ±2.8 | 19.1 ±5.9 | 20.6 ±5.8 | 17.9 ±5.3 |
| Opus @ 12 kbps | 76.5 ±2.3 | 61.9 ±2.1 | 77.8 ±3.2 | 65.4 ±2.7 |
| EnCodec @ 3 kbps | 67.0 ±1.5 | 62.5 ±2.3 | 89.6 ±3.1 | 87.8 ±2.9 |
| EnCodec @ 6 kbps | 83.1 ±2.7 | 69.4 ±2.3 | 92.9 ±1.8 | 91.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 paper’s entire system is three boxes and a training objective. Fix the names now; the rest of the lesson opens each box.
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.
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:
| Stage | Shape | Type | Note |
|---|---|---|---|
| input x | [B, 1, 24000] | float in [−1, 1] | 1 channel, 24,000 samples |
| after encoder E | [B, D, 75] | float32 | 320× 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] | float32 | sum 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.
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.
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.
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.
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:
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.
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 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.
—
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:
| Band | Level (dB) | Masking threshold (dB) | Headroom = level − threshold | Bits allocated |
|---|---|---|---|---|
| 0 | 42 | 30 | +12 | 2 |
| 1 | 55 | 52 | +3 | 1 |
| 2 (masker) | 78 | — | — | 4 |
| 3 | 61 | 65 | −4 | 0 |
| 4 | 47 | 52 | −5 | 0 |
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:
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."
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.
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.
| Codec | Standardised | Range | Designed for |
|---|---|---|---|
| Opus | IETF, 2012 | 6 kbps narrowband mono → 510 kbps fullband stereo | Everything. A hybrid of a speech coder (SILK) and a transform coder (CELT), switching or blending by bitrate and content. |
| EVS | 3GPP, 2014 | 5.9 to 128 kbps, audio bandwidth 4 kHz to 20 kHz | Voice over LTE. Successor to AMR-WB. Speech-first, and it shows: EVS is the strongest baseline on clean speech in the paper. |
| MP3 | ISO, 1993 | Used at 64 kbps in the paper’s stereo table | The 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 v2 | Google, 2022 | 3.2 and 6 kbps in the paper’s tests | The neural baseline. It is the official SoundStream implementation, evaluated on audio upsampled to 32 kHz. |
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:
| Work | The move | What it left open |
|---|---|---|
| Morishima et al. 1990 | Neural networks as trained transforms in an encoder/decoder | Three decades early; no compute, no synthesis quality |
| WaveNet (Oord et al. 2016) | Autoregressive raw-waveform synthesis that finally sounded real | Sample-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 quantizer | Features still hand-designed; not end to end |
| VQ-VAE + WaveNet (Gârbacea et al. 2019) | Discrete units from a VQ-VAE, decoded by WaveNet | A 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 speed | Vocoders, not codecs — no learnt discrete bottleneck |
| SoundStream (Zeghidour et al. 2021) | The direct ancestor: fully convolutional encoder/decoder + residual vector quantization, reconstruction + adversarial losses | Complicated 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.
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.
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.
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:
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.
| # | Layer | Channels | Time steps | Why |
|---|---|---|---|---|
| 0 | input waveform | 1 | 24000 | mono, in [−1, 1] |
| 1 | Conv1d, kernel 7 | 1 → 32 | 24000 | stride 1: lift to C channels without touching time |
| 2 | residual unit (2 convs, k=3, skip) | 32 | 24000 | local nonlinear processing at full rate |
| 3 | strided conv, S=2, K=4 | 32 → 64 | 12000 | first downsample; channels double |
| 4 | residual unit | 64 | 12000 | |
| 5 | strided conv, S=4, K=8 | 64 → 128 | 3000 | |
| 6 | residual unit | 128 | 3000 | |
| 7 | strided conv, S=5, K=10 | 128 → 256 | 600 | |
| 8 | residual unit | 256 | 600 | |
| 9 | strided conv, S=8, K=16 | 256 → 512 | 75 | 2·4·5·8 = 320 total; 24000/320 = 75 |
| 10 | two-layer LSTM | 512 | 75 | sequence modelling over the latent, shapes unchanged |
| 11 | Conv1d, kernel 7 | 512 → D | 75 | project 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.
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 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.
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.
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.
What does streaming cost in quality? Table 3, at 6 kbps on an equal mix of speech and music:
| Model | Streamable | SI-SNR | ViSQOL |
|---|---|---|---|
| Opus | yes | 2.45 | 2.60 |
| EVS | yes | 1.89 | 2.74 |
| EnCodec | yes | 6.67 | 4.35 |
| EnCodec | no | 7.46 | 4.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.
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.
—
"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.
| Component | 24 kHz streaming | Where it comes from |
|---|---|---|
| Frame size | 320 samples = 13.3 ms | The total stride. You cannot emit an output until a whole frame of input exists. |
| Look-ahead | 0 ms | All padding is on the left, so no future samples are needed. This is the causal-padding payoff. |
| Normalisation window | 0 ms | Weight normalization depends on weights, not on activations. The non-streamable variant pays 1 s here. |
| Compute | ≈ 1.3 ms per frame | RTF 10 means 13.3 ms of audio takes about 1.3 ms to process. |
| Entropy coding | +13 ms if enabled | The stream cannot be flushed each frame, so decoding frame t needs frame t+1 partially received. |
| Total, no entropy coding | ≈ 14.6 ms | Well 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.
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.
| Variant | RTF encode | RTF decode | SI-SNR | ViSQOL |
|---|---|---|---|---|
| EnCodec base (C=32, 1 res unit, LSTM) | 9.8 | 10.4 | 6.67 | 4.35 |
| Channels = 16 | 26.0 | 25.7 | 6.40 | 4.32 |
| Channels = 64 | 1.3 | 3.1 | 6.70 | 4.38 |
| norm = None | 10.1 | 10.4 | 6.45 | 4.29 |
| LSTM = 0 | 15.0 | 14.6 | 6.40 | 4.35 |
| Residual layers = 3, LSTM = 0 | 6.0 | 7.3 | 6.32 | 4.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."
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.
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?
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 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:
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.
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.
—
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:
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:
| Wall | Why |
|---|---|
| Memory | Storing 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. |
| Search | Encoding requires a nearest-neighbour search over N entries per frame, 75 times a second, in real time on one CPU core. |
| Training data | Every 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.
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:
Substitute N = 2b for b bits and take logarithms:
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.
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.
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.
Take D = 2 so the arithmetic is visible, and a tiny codebook of four entries:
and the latent z = (0.9, −0.4). Compute all four squared distances, every term shown:
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%.
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.
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.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."
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.
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:
Multiplicative in the exponent, additive in storage. Put EnCodec’s numbers in: N = 1024, Nq = 32.
| Quantity | Single codebook | RVQ (32 × 1024) |
|---|---|---|
| Bits per frame | 320 | 320 |
| Reachable points | 2320 ≈ 1096 | 102432 = 2320 ≈ 1096 |
| Vectors you must store | 1096 | 32,768 |
| Distance computations per frame | 1096 | 32 × 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.
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:
Squared distances, recomputed so this chapter stands alone:
So k1 = 0, and the new residual is:
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:
Squared distances from r2 = (−0.1, −0.4), every term written out:
So k2 = 1, and:
The reconstruction. The decoder receives the two integers (0, 1) and sums the corresponding entries — it never sees a residual:
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:
| Stage | Bits spent | Residual norm | Relative error | Improvement |
|---|---|---|---|---|
| 0 (nothing sent) | 0 | 0.9849 | 100% | — |
| 1 | 2 | 0.4123 | 41.9% | 2.39× |
| 2 | 4 | 0.1000 | 10.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.
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:
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.
—
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.
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."
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.
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.
—
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)
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.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.
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 get | Why |
|---|---|---|
| Time-domain L1 or L2 | Dull, 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 distance | Correct spectral envelope, but buzzy / metallic. | Magnitude spectrograms discard phase. Many phase assignments give the same magnitude and most of them sound wrong. |
| Adversarial loss only | Plausible 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:
| Symbol | Name | What it measures | λ at 24 kHz |
|---|---|---|---|
| ℓt | Time-domain loss | L1 between waveforms | 0.1 |
| ℓf | Frequency-domain loss | Multi-scale mel-spectrogram L1 + L2 | 1 |
| ℓg | Generator adversarial loss | Hinge loss against the discriminators | 3 |
| ℓfeat | Relative feature matching | Distance between discriminator internal activations | 3 |
| ℓw | VQ commitment loss | Encoder output vs its quantized value | see Ch 6 — it sits outside the balancer |
At 48 kHz the paper changes only two: λg = 4 and λfeat = 4.
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.
This is Equation 1, and it looks worse than it is:
| Symbol | Meaning | Concrete value in EnCodec |
|---|---|---|
| Si | A 64-bin mel-spectrogram computed with a normalized STFT | window size 2i, hop length 2i/4 |
| e | The set of scales, i.e. the exponents | e = 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 scales | the count of (α, scale) pairs |
Write out the seven scales explicitly, because "multi-scale" stays abstract until you do:
| i | Window = 2i | Hop = 2i/4 | Window duration at 24 kHz | Sees |
|---|---|---|---|---|
| 5 | 32 | 8 | 1.33 ms | transients, click onsets — near-perfect time resolution, almost no frequency resolution |
| 6 | 64 | 16 | 2.67 ms | plosives, drum attacks |
| 7 | 128 | 32 | 5.33 ms | fast formant motion |
| 8 | 256 | 64 | 10.7 ms | the classic speech-analysis window |
| 9 | 512 | 128 | 21.3 ms | pitch and harmonic structure |
| 10 | 1024 | 256 | 42.7 ms | timbre, low-frequency detail |
| 11 | 2048 | 512 | 85.3 ms | bass notes, room tone, sustained resonance |
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)".
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.
—
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:
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:
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
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.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:
| Property | Value |
|---|---|
| Number of sub-discriminators K | 5, one per STFT scale |
| STFT window lengths | [2048, 1024, 512, 256, 128], each with hop = window / 4 |
| Input | The complex-valued STFT, with real and imaginary parts concatenated as channels |
| First layer | Conv2D, kernel 3 × 8, 32 channels |
| Middle layers | Conv2D with dilation in the time dimension of 1, 2, 4 and stride 2 over the frequency axis |
| Final layer | Conv2D kernel 3 × 3, stride (1, 1) → the prediction map |
| Activation / norm | LeakyReLU, weight normalization |
| 48 kHz variant | Double every STFT window size; train the discriminator every two batches |
| Stereo | Left and right channels processed separately |
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:
and the discriminators minimise the corresponding hinge:
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.
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:
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.
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 setup | SI-SNR | ViSQOL | MUSHRA (human) |
|---|---|---|---|
| MSD + Mono-STFT (the SoundStream stack) | 5.99 | 4.22 | 62.91 ±2.62 |
| MPD only | 7.35 | 4.24 | 60.7 ±2.8 |
| MS-STFT + MPD | 6.55 | 4.34 | 79.0 ±1.9 |
| MS-STFT only (EnCodec) | 6.67 | 4.35 | 77.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.
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.
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.
—
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.
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."
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 | ‖gi‖2 | λi · ‖gi‖ | Actual share | Intended share (λi / ∑λ) |
|---|---|---|---|---|---|
| ℓt (time L1) | 0.1 | 0.02 | 0.1 × 0.02 = 0.002 | 0.03% | 0.1/7.1 = 1.41% |
| ℓf (mel) | 1 | 3.00 | 1 × 3.00 = 3.000 | 38.5% | 1/7.1 = 14.08% |
| ℓg (adversarial) | 3 | 0.40 | 3 × 0.40 = 1.200 | 15.4% | 3/7.1 = 42.25% |
| ℓfeat (feature match) | 3 | 1.20 | 3 × 1.20 = 3.600 | 46.1% | 3/7.1 = 42.25% |
| total | 7.1 | — | 7.802 | 100% | 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 heavily (λg = 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:
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.
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̂:
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 ⟨‖gi‖2⟩β be the exponential moving average of that gradient norm over recent training batches, with β = 0.999. Then gi / ⟨‖gi‖2⟩β 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:
and backpropagate ∑i g̃i 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:
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."
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.
—
"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 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 | λfeat | Balancer | SI-SNR | ViSQOL |
|---|---|---|---|---|---|---|
| 1 | 1 | 1 | 1 | yes | 10.32 | 4.16 |
| 1 | 1 | 1 | 1 | no | 6.16 | 3.89 |
| 1 | 2 | 4 | 1 | yes | 9.93 | 4.17 |
| 1 | 2 | 4 | 1 | no | 1.72 | 3.52 |
| 1 | 2 | 100 | 1 | yes | 8.41 | 4.05 |
| 1 | 2 | 100 | 1 | no | −35.83 | 2.82 |
| 10 | 2 | 100 | 4 | yes | 9.22 | 4.09 |
| 10 | 2 | 100 | 4 | no | −16.39 | 2.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.
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.
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.
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.
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:
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:
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:
So the same symbol stream now costs 1.533 bits instead of 2. The saving:
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.
"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."
| Property | Value | Why it is that small |
|---|---|---|
| Layers | 5 | The 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 heads | 8 | |
| Model width | 200 channels | |
| Feed-forward dimension | 800 | |
| Dropout | none | The dataset is effectively unbounded — every second of training audio is new code sequences |
| Causal receptive field | 3.5 seconds | ≈ 262 frames at 75 Hz; audio structure beyond a few seconds is not predictive of individual codes |
| Training sequence length | 5 seconds | Longer than the receptive field, so every position sees full context |
| Position embeddings | Sinusoidal, 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 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."
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.
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]:
Bits required to name a point in an interval of width w is log2(1/w):
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.
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.
—
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.
| Defence | Value | Effect |
|---|---|---|
| Round the estimated probabilities | precision 10−6 | Two orders of magnitude coarser than the observed 10−8 discrepancy, so both sides round to the same value |
| Total range width | 224 | Integer arithmetic, not floats, inside the coder itself — exactly reproducible |
| Minimum range width | 2 | No 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 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:
| Nominal | After entropy coding | Saving | Setting |
|---|---|---|---|
| 1.5 kbps | 0.9 kbps | 40.0% | 24 kHz mono |
| 3.0 kbps | 1.9 kbps | 36.7% | 24 kHz mono |
| 6.0 kbps | 4.1 kbps | 31.7% | 24 kHz mono |
| 12.0 kbps | 8.9 kbps | 25.8% | 24 kHz mono |
| 6.0 kbps | 4.2 kbps | 30.0% | 48 kHz stereo |
| 12.0 kbps | 8.9 kbps | 25.8% | 48 kHz stereo |
| 24.0 kbps | 19.4 kbps | 19.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:
| Model | Latency | Enc. | Dec. | Enc. + EC | Dec. + EC |
|---|---|---|---|---|---|
| Lyra v2 (32 kHz) | — | 27.4 | 67.2 | — | — |
| EnCodec 24 kHz | 13 ms | 9.8 | 10.4 | 1.6 | 1.6 |
| EnCodec 48 kHz | 1 s | 6.8 | 5.1 | 0.68 | 0.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.
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.
—
Everything so far has been mechanism. This chapter is evidence: what the model was trained on, how, and what happened when humans listened.
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:
| Domain | Datasets | Role |
|---|---|---|
| Speech | Clean segments from DNS Challenge 4; Common Voice | The bread-and-butter case, and the one where classical codecs are strongest |
| General audio | AudioSet; FSD50K | Environmental sound, sound events — the long tail that breaks psychoacoustic models |
| Music | MTG-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 speech | Created on the fly — see the mixing strategy below | Realistic 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:
| Strategy | What it does | Probability |
|---|---|---|
| s1 | Sample a single source from Jamendo (music alone) | 0.32 |
| s2 | Sample a single source from the other datasets | 0.32 |
| s3 | Mix two sources from all datasets | 0.24 |
| s4 | Mix three sources from all datasets except music | 0.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:
| Setting | Value |
|---|---|
| Epochs | 300 |
| Updates per epoch | 2,000 → 600,000 total updates |
| Optimizer | Adam, β1 = 0.5, β2 = 0.9 |
| Learning rate | 3 × 10−4 |
| Batch | 64 examples of 1 second each |
| Hardware | 8 × 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.
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:
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-SNR | ViSQOL | MUSHRA | |
|---|---|---|---|
| What it is | Waveform energy ratio | Regression fitted to human ratings | Actual humans |
| Range in this paper | 1.89 to 7.46 dB | 2.60 to 4.39 (of 5) | 17.7 to 97.1 (of 100) |
| Cost per evaluation | microseconds | seconds | weeks and money |
| Sees phase? | Yes — and over-weights it | Partly | As humans do |
| Trustworthy for… | Detecting gross breakage | Ranking similar systems | Everything, but you cannot afford it often |
Table 1, in full, streamable setting, 24 kHz. Mean MUSHRA with 95% confidence intervals:
| Model | Bandwidth | Entropy coded | Clean speech | Noisy speech | Music set 1 | Music set 2 |
|---|---|---|---|---|---|---|
| Reference | — | — | 95.5 ±1.6 | 93.9 ±1.8 | 93.2 ±2.5 | 97.1 ±1.3 |
| Opus | 6.0 kbps | — | 30.1 ±2.8 | 19.1 ±5.9 | 20.6 ±5.8 | 17.9 ±5.3 |
| Opus | 12.0 kbps | — | 76.5 ±2.3 | 61.9 ±2.1 | 77.8 ±3.2 | 65.4 ±2.7 |
| EVS | 9.6 kbps | — | 84.4 ±2.5 | 80.0 ±2.4 | 89.9 ±2.3 | 87.7 ±2.3 |
| Lyra-v2 | 3.0 kbps | — | 53.1 ±1.9 | 52.0 ±4.7 | 69.3 ±3.3 | 42.3 ±3.5 |
| Lyra-v2 | 6.0 kbps | — | 66.2 ±2.9 | 59.9 ±3.3 | 75.7 ±2.6 | 48.6 ±2.1 |
| EnCodec | 1.5 kbps | 0.9 kbps | 49.2 ±2.4 | 41.3 ±3.6 | 68.2 ±2.2 | 66.5 ±2.3 |
| EnCodec | 3.0 kbps | 1.9 kbps | 67.0 ±1.5 | 62.5 ±2.3 | 89.6 ±3.1 | 87.8 ±2.9 |
| EnCodec | 6.0 kbps | 4.1 kbps | 83.1 ±2.7 | 69.4 ±2.3 | 92.9 ±1.8 | 91.3 ±2.1 |
| EnCodec | 12.0 kbps | 8.9 kbps | 90.6 ±2.6 | 80.1 ±2.5 | 91.8 ±2.5 | 92.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.
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.
—
Table 4 evaluates 48 kHz stereo music, where the reference is 1,536 kbps and the compression ratios get absurd:
| Model | Bandwidth | Entropy coded | Compression | MUSHRA |
|---|---|---|---|---|
| Reference | — | — | 1× | 95.1 ±1.8 |
| MP3 | 64 kbps | — | 24× | 82.7 ±3.2 |
| Opus | 6 kbps | — | 256× | 17.7 ±5.9 |
| Opus | 24 kbps | — | 64× | 82.9 ±3.7 |
| EnCodec | 6 kbps | 4.2 kbps | 256× | 82.9 ±2.4 |
| EnCodec | 12 kbps | 8.9 kbps | 128× | 88.0 ±2.7 |
| EnCodec | 24 kbps | 19.4 kbps | 64× | 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.
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:
| Model | Bandwidth | MUSHRA |
|---|---|---|
| Reference | — | 96.1 ±1.41 |
| Opus | 6.0 kbps | 21.1 ±2.62 |
| EVS | 6.0 kbps | 62.9 ±2.18 |
| SoundStream (re-implemented) | 3.0 kbps | 71.8 ±1.51 |
| EnCodec with DiffQ quantizer | 3.0 kbps | 72.3 ±1.18 |
| EnCodec with RVQ | 3.0 kbps | 76.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.
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.
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.
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.
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 tokens | Semantic tokens | |
|---|---|---|
| Produced by | SoundStream / EnCodec RVQ | Clustering self-supervised features (w2v-BERT, HuBERT) |
| Trained for | Reconstruction — every acoustic detail | Prediction of masked content — linguistic and structural content |
| Preserve | Speaker identity, timbre, room, prosody | Phonetic and semantic content |
| Discard | Nothing perceptually important | Almost all acoustic detail |
| Good for | Faithful resynthesis | Long-range coherence in generation |
| Bad at | Long-horizon structure — too many tokens, too much detail | Sounding 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.
| System | Relationship to EnCodec | The new idea |
|---|---|---|
| AudioLM (2022) | Uses SoundStream RVQ acoustic tokens; EnCodec is the open, better-performing sibling | Semantic + acoustic token hierarchy, generated in three stages |
| MusicGen (2023) | Built directly on EnCodec by the same lab | Codebook 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 systems | EnCodec as the decoder for generated tokens | Text conditioning over audio token streams |
| Mimi / Moshi (2024) | A streaming codec in EnCodec’s lineage | Distil 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.
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.
| Misconception | Correction |
|---|---|
| "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. |
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.
The paper is unusually candid, and its limits are worth collecting in one place:
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.
| Step | Build | How you know it works |
|---|---|---|
| 1 | Encoder and decoder with no quantizer, trained on time-domain L1 alone | The autoencoder reconstructs near-perfectly. If it cannot, your strides or padding are wrong — check that output length equals input length exactly. |
| 2 | Add the multi-scale mel loss | Output 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). |
| 3 | Add a single VQ layer | Watch 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. |
| 4 | Extend to RVQ with Nq stages | Log 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. |
| 5 | Randomise Nq per batch | Evaluate at every supported bandwidth. Quality must degrade monotonically as you truncate. |
| 6 | Add the MS-STFT discriminator and the balancer together | Adding the adversary without the balancer is where training collapses. Table A.4 is the evidence; do not learn it the expensive way. |
| 7 | Convert to causal padding, swap layer norm for weight norm | Encode 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. |
| 8 | Train the entropy model, add the arithmetic coder | Round-trip test on thousands of frames. Any single mismatch corrupts everything after it, so a partial pass is a fail. |
Every symbol, in one table. If you can reconstruct the paper from this, you have the lesson.
| Symbol | Meaning | Value / shape in EnCodec |
|---|---|---|
| x | Input audio | [Ca, T] in [−1, 1]; T = d · fsr |
| x̂ | Reconstructed audio | same shape as x |
| E, Q, G | Encoder, quantizer, decoder | the three components |
| z | Continuous latent | [B, D, T'] where T' = T / 320 |
| zq | Quantized latent (sum of chosen entries) | [B, D, T'], float |
| C, B | Base channels, number of conv blocks | C = 32, B = 4 |
| S, K | Stride, kernel size of a downsample | strides (2, 4, 5, 8); K = 2S |
| Nq | Number of RVQ codebooks used | 2, 4, 8, 16, 32 (max 16 at 48 kHz) |
| codebook size | Entries per codebook | 1024 = 10 bits |
| frame rate | Latent steps per second | 75 at 24 kHz, 150 at 48 kHz |
| ℓt | Time-domain loss | L1 between waveforms; λt = 0.1 |
| ℓf | Multi-scale mel loss | 64 mel bins, windows 25…211, hop = window/4, αi = 1; λf = 1 |
| ℓg | Generator adversarial hinge | (1/K) ∑ max(0, 1 − Dk(x̂)); λg = 3 (4 at 48 kHz) |
| ℓfeat | Relative feature matching | Eq. 2, normalised per layer; λfeat = 3 (4 at 48 kHz) |
| ℓw | Commitment loss | ∑c ‖zc − qc(zc)‖22; outside the balancer |
| Ld | Discriminator loss | hinge on both real and fake; update prob. 2/3 (0.5 at 48 kHz) |
| K | Number of sub-discriminators | 5, windows [2048, 1024, 512, 256, 128] |
| gi | Gradient of loss i w.r.t. x̂ | the balancer’s input |
| g̃i | Balanced gradient | R · (λi/∑λj) · gi / ⟨‖gi‖⟩β |
| R, β | Reference norm, EMA decay for the balancer | R = 1, β = 0.999 |
| EMA decay (codebook) | Codebook entry update rate | 0.99 |
| LM | Entropy-coding Transformer | 5 layers, 8 heads, 200 channels, FFN 800, 3.5 s receptive field |
| coder settings | Arithmetic coder | range width 224, min width 2, probs rounded to 10−6 |
If you can answer these without scrolling up, you own the paper.
| Direction | Lesson | Why |
|---|---|---|
| ← Prerequisite | Neural Audio Codecs | The family overview: SoundStream, EnCodec, DAC, Mimi, and where each sits |
| ← Foundation | Audio Representations | STFT, mel filterbanks, and why phase is the hard part — the substrate of Chapter 5 |
| → Next | AudioLM | Audio as a language: semantic plus acoustic tokens, three-stage generation |
| → Applied | Music Generation | MusicGen’s codebook interleaving patterns, built directly on EnCodec |
| → Sibling | TTS Architectures | Where neural vocoders came from and how codec tokens changed speech synthesis |
| → Contrast | CLAP | The other way to make audio machine-readable: continuous embeddings aligned with text, rather than discrete tokens |