MP2 — MPEG-1 Audio Layer II — is the quiet workhorse of broadcast audio. It is the sound of digital radio (DAB) and a great deal of digital TV, and plenty of playout and archive systems still want it. Browsers can read a lot of formats, but none of them can write MP2: WebCodecs has no MP2 encoder, and the JavaScript MP3 encoders make Layer III, a different format. When we went looking for an MP2 encoder for a web tool, we found C (TwoLAME) and FFmpeg, and JavaScript code that only decodes. So we wrote one and we are releasing it: micromp2.js, MIT licensed, on GitHub.
Using it
<script src="micromp2.js"></script>
<script>
const mp2 = micromp2.encode({ channels: [left, right], sampleRate: 44100, bitrate: 192 });
const blob = new Blob([mp2], { type: "audio/mpeg" }); // mp2 is a Uint8Array: a raw .mp2 file
</script>
It takes one typed array per channel (Float32Array or Int16Array) or one interleaved array, and returns the finished file. There is an async version that yields to the event loop, a streaming encoder for feeding audio in pieces, a decoder for the same format, and an ES-module build. It has no DOM access, so it also runs in a Web Worker and in Node.
What it does
Mono and stereo, constant bitrate, at 16, 22.05, 24, 32, 44.1 and 48 kHz (MPEG-1 and its MPEG-2 half-rate extension), Layer II by default and Layer I on request, with joint stereo, an optional CRC on every frame, and bitrates snapped to whatever the standard allows. Every frame is the same size, so the bitrate is exactly what you asked for: in our tests the file was exactly bitrate × frames × 1152 ÷ sample rate ÷ 8 bytes in all seven combinations we measured.
How it works
Layer II splits the sound into 32 frequency bands, groups 1152 samples into a frame, and decides, band by band, how finely to round the numbers. The art is in spending a fixed budget of bits where the ear will notice least. For each band we measure a scalefactor (the band's loudness for each third of the frame), estimate how much each band masks its neighbours on the Bark scale, compare with the threshold in quiet, and then hand out the budget one step at a time to whichever band has its noise closest to audible. The result is packed into a frame that carries everything a decoder needs.
The numbers
Measured in Node 24 on one Windows PC with synthetic signals (a voice-like harmonic signal and decaying chord tones); npm run measure in the repository regenerates every number and figure here.
| What | Result |
|---|---|
| Code size | 50.1 KB as shipped (with licence and comments), 35.9 KB stripped of comments, 14.8 KB gzipped (10.8 KB stripped and gzipped), for encoder, decoder and tables together. Without the decoder: about 8.4 KB gzipped (an estimate) |
| Speed, 10 s of 44.1 kHz stereo at 192 kbps | encode about 262 ms (38× real time), decode about 51 ms |
| Speed, 10 s mono at 128 kbps | encode about 91 ms (110× real time), decode about 28 ms |
| Constant bitrate | file size exact to the byte in 7 of 7 combinations |
| Signal-to-noise ratio, mono 32 → 192 kbps | about 10 → 27 dB (voice-like), 15 → 36 dB (chord tones) |
How we tested it — and what we have not
The test page in the download runs 46 checks in your own browser, offline (and the same ones in Node). For eight sample-rate, channel and bitrate combinations an independent frame walker, with its own tables rather than the library's, confirms the output is a sequence of valid Layer II frames with the right count, bitrate, rate and channel mode. Round trips through the bundled decoder check the format, a signal-to-noise floor, that more bits mean a closer match, and that stereo channels stay separate. Streaming in odd-sized pieces, the async API, interleaved input and 16-bit integer input must give exactly the same bytes as one call; the CRC option must produce frames the decoder accepts and a damaged frame must be caught; and the decoder must survive an ID3 tag, leading junk and a cut-off last frame. Everything passed.
The honest limits: the sound is unmeasured against other encoders, there is no listening test, and no second decoder was used on the files in this repository — the browsers we tried cannot decode raw MP2, so the page's check against the browser's own decoder skips itself. It is constant bitrate only, one or two channels, no MPEG-2.5 rates and no Layer III, and it writes raw frames without tags or a container. If a player rejects one of our files, we want to know.
Download
The source is on GitHub: github.com/dialersapps/micromp2 (issues and pull requests welcome). The downloads below are plain files with nothing to build, no tracking and no outside requests:
- micromp2-1.0.0.zip — everything in one file (library, ES module, demo, tests, measurement tools, figures, README, licence)
- micromp2.js — the library (plain script tag,
require()and Workers) - micromp2.mjs — the same code as an ES module
- demo.html — encode your own audio and listen (everything stays in your browser)
- test.html — the in-browser test suite
- README.md — API, options, limits
- LICENSE — MIT, copyright Dialer's Apps 2026
The code was written by Dialer's Apps from the published MPEG standards (the numeric tables are the standards' own data); it is not derived from TwoLAME, FFmpeg or any other encoder, and the masking model is our own simple design.