rmps

MPEG-1/2/2.5 Layer III encoder · C++20 · WebAssembly

Spend bits where you can hear them.

rmps searches for the quantization that sounds best, not the one that happens to fit. A beam search over scalefactors and global gain, scored against a masking model rather than raw error, decides every band of every granule.

bits spent per frame masking threshold bit reservoir 1 column = 1152 samples = 26 ms

What it does differently

Every MP3 encoder answers the same question — how coarsely can each band be quantized before it becomes audible. These are the four places rmps answers it differently.

Quantization

Beam search, not a greedy loop

Scalefactors and global gain are searched jointly, keeping several candidate paths alive per band instead of committing to the first one that fits the budget. Width is set by the effort level.

Cost function

Scored on masking, not error

The search minimizes a mix of noise-to-mask ratio and A-weighted SDR. Distortion under the threshold is free; distortion above it is what gets paid for.

Rate control

A quality target, not a bitrate

Ask for a perceptual SDR and the encoder moves the bitrate across the full legal range to hold it. CBR and PID-controlled ABR are there when you need a size instead.

Transients

Short blocks only when they help

Window switching triggers on high-frequency attacks — hats, snare snaps, cymbals. Bass-heavy hits stay on long blocks, where they keep the frequency resolution their sustain needs.

The pipeline

Each stage feeds the next; the masking model is what makes the last two stages differ from a textbook encoder.

  1. Polyphase subband filter32 bands, plus a one-frame lookahead so the reservoir knows what is coming.
  2. MDCT18 lines per band, long or short depending on the transient gate.
  3. Masking modelPer-scalefactor-band thresholds, ATH floor, spreading, and pre-echo control.
  4. Mid/side decisionPer frame, on measured bit cost — not a fixed mode.
  5. Beam searchScalefactors, global gain and noise shaping against the threshold.
  6. Bit reservoir & bitstreamHuffman coding, reservoir borrowing, Xing/Info seek table.

Use it in the browser

The whole encoder compiles to WebAssembly. Audio is read, encoded and downloaded on the page — there is no upload and no server.

One shot

import { encodeAudioBuffer } from './rmps.js';

const ac  = new AudioContext();
const buf = await ac.decodeAudioData(bytes);

const blob = await encodeAudioBuffer(buf, {
  quality: 70,        // 1..100
  mode: 'jointStereo',
  onProgress: ({ ratio }) => bar.value = ratio,
});

Encoding is CPU-bound and single-threaded. Run it in the bundled module worker unless the clip is very short — a few minutes of audio is seconds of solid compute, and the main thread will visibly freeze.

Streaming

const enc = await RmpsEncoder.create({
  channels: 2, sampleRate: 44100, bitrate: 192,
});

for (let p = 0; p < L.length; p += enc.samplesPerPass) {
  sink(enc.encode(L.subarray(p, p + spp),
                  R.subarray(p, p + spp)));
}
const blob = enc.finish();   // flush + Xing header
enc.destroy();

encode() returning zero bytes is normal. The encoder holds one frame back for lookahead, and reservoir emission lags. Nothing is dropped — the bytes arrive on a later call or in finish(), which also rewrites the Xing header with the real duration and seek table.

Specifications

Format

Standard
MPEG-1/2/2.5 Layer III
Sample rates
8 – 48 kHz
Channels
mono, stereo, joint, dual
Rate modes
CBR, ABR, quality VBR
Seek table
Xing / Info, 100 points

Build

Language
C++20
Dependencies
none
Targets
CLI, static lib, wasm
Browser build
emscripten + CMake
License
MIT

Try the encoder