Push-to-talk radio apps, phone calls and voice notes all squeeze speech through a narrow pipe, and the first thing the pipe throws away is the top end. What is left is intelligible but dull: the s and sh sounds lose their hiss, the breath and the air around a voice disappear, and the talker sounds like they are in the next room. We wanted a way to put some of that back, in the browser, with no binary to load. So we wrote one and we are releasing it: sbr-crystalizer.js, MIT licensed, on GitHub.

Read this first. It is a perceptual enhancer, not a reconstruction. It adds plausible high-frequency content above the point where a recording was cut off; it cannot recover what the recording lost. It is built for speech, and everything below was measured on a synthetic, seeded voice-like signal, not on a corpus of real speech, and with no listening test. Use the demo page to judge it by ear on your own recordings.

What goes missing, and what we do about it

A recording that went through a low-bitrate codec, a telephone line or a sloppy resampler usually has a hard ceiling: nothing above, say, 8 kHz. Speech has plenty to say above that, mostly noise-like sounds (fricatives, breath), which is exactly the kind of content that is easy to imitate convincingly. The idea, known in codecs as spectral band replication (it is how HE-AAC gets its sparkle at low bitrates), is to look at what is just below the ceiling and build a band above it that has a similar level and a speech-like fall-off.

Codecs do that at the encoder's request, from hints. We do it blind, on a finished file: the library finds the ceiling by itself, so you can hand it any clip.

Spectrograms at 48 kHz: a wideband original, the same clip low-passed at 8 kHz, and the processed result, where the band above the detected 7.8 kHz cutoff is rebuilt up to about 15 kHz
Figure 1. Top: a wideband original. Middle: the same clip cut at 8 kHz, as a codec or resampler would. Bottom: after sbr-crystalizer.js, with the detected cutoff marked. The new band is built above it and tapers out near 15 kHz. (Synthetic voice-like signal, seed 20261016.)

Using it

<script src="sbr-crystalizer.js"></script>
<script>
  // channels: Float32Array[] (for example from AudioBuffer.getChannelData)
  const r = await sbrCrystalizer.process(channels, sampleRate);
  if (r.applied) play(r.channels, r.sampleRate);   // otherwise r.channels is your input, untouched
</script>

It is one file with no dependencies and no DOM access, so it also runs in a Web Worker, an Android WebView and Node, with an ES-module build alongside. It is asynchronous so a long clip does not freeze a page, and it accepts an abort signal. The options are few: a manual cutoff, how loud and how steep the new band is, and a switch to output at double the sample rate so a 16 kHz clip has somewhere to put its new top end.

How it works

Pass one reads the whole clip and finds the ceiling: the steepest cliff in the average spectrum. Pass two goes frame by frame. Everything below the ceiling is copied through unchanged. Above it, the library measures the level just under the ceiling, extends it upward with a speech-like downward slope and a long-term speech-spectrum floor (so a codec's hollow top is not left hollow), shapes it with the texture of the band just below, gives it pseudo-random phases so it behaves like noise, switches it on only when the frame is loud enough to be speech rather than room noise, tapers the very top, and transforms back. The "random" phases are a hash of the frame and bin number, so the same clip always produces the same samples.

Pipeline: pass one analyses the whole clip (STFT, average spectrum, cutoff, frame energies). Pass two, per frame: keep the real spectrum, extend upward with tilt and speech floor, shape and add noise, gate by energy, inverse FFT, overlap-add.
Figure 2. The two passes. Green boxes are where the output is assembled.

When it does nothing

A tool like this is only as trustworthy as its willingness to leave things alone. If a clip is already full-band, is mostly silence or noise, is very short, has a sample rate outside 8 to 48 kHz, or has no steep cutoff, you get your input back (the very same arrays, with a reason). Only when all the checks pass does it rebuild anything.

Decision diagram: audio is only changed when the sample rate is 8 to 48 kHz, speech is present and a cutoff is found
Figure 3. The checks, in order. Auto-detection needs a steep cutoff (at least 15 dB over about 375 Hz), the kind codecs and resamplers leave.

The numbers

Measured in Node 24 on one Windows PC, on a synthetic, seeded voice-like signal (vowel-like formants plus fricative-like noise); npm run measure in the repository regenerates every number and figure here. The setup: a wideband original is cut with an ideal low-pass at a known frequency, processed, and compared with the original.

CaseCutoff foundResult
48 kHz, ideal cut at 8 kHz7.8 kHz9–14 kHz band rebuilt to about −71 dB (original −58 dB, after the cut −157 dB). Log-spectral distance to the original in that band: 91.5 dB before, 12.1 dB after
16 kHz, ideal cut at 3.8 kHz3.6 kHz4.3–7 kHz band rebuilt to about −78 dB (original −60 dB, after the cut −172 dB). Distance 101.0 dB before, 15.1 dB after
48 kHz, gentle roll-off at 8 kHznot found (auto)With the cutoff supplied by hand: 36.2 dB before, 13.1 dB after
Full-band signal, or noise onlynoneReturned untouched
Long-term average spectrum at 48 kHz: the original, the low-passed copy and the processed copy, with the processed curve rejoining the original's slope above 8 kHz but about 13 dB lower
Figure 4. Long-term average spectrum. The rebuilt band sits about 13 dB below the original on purpose: it is a plausible fill, not a copy, and it tapers rather than hiss.
Spectrograms at 16 kHz: original, low-passed at 3.8 kHz and processed
Figure 5. The same test at 16 kHz sampling, with the original cut at 3.8 kHz (a phone-like clip). The gaps in the rebuilt band are the energy gate keeping the new top end out of the quiet frames.
  • Below the cutoff the audio is unchanged: mean absolute difference 0.0002 dB (48 kHz) and 0.0013 dB (16 kHz).
  • Deterministic: processing a clip twice gives identical samples, and identical left and right channels stay identical.
  • Speed: about 270 ms for 10 s of mono at 48 kHz, about 85 ms at 16 kHz.
  • Size: about 16 KB with licence and comments, about 11 KB stripped of comments, 6.3 KB gzipped as shipped (about 4.1 KB stripped and gzipped).

Log-spectral distance says how close the spectral envelope is to the original; it does not say how it sounds. We have not run a listening test.

How we tested it — and what we have not

The repository has 23 automated checks that run both in the browser (test.html) and in Node (npm test) on seeded synthetic signals: cutoff detection, gain above the cutoff, a transparent region below it, input arrays left unmodified, determinism, stereo behaviour, pass-through for noise, silence, short clips, bad sample rates and empty input, manual cutoff, double-rate output, the yield hook and abort signals, and that the ES module matches the script build. All pass.

The honest limits: it is for speech, and we do not recommend it for music, because the new band is a speech-shaped noise fill. Auto-detection needs a steep cutoff, so a gentle filter roll-off is missed (give it the cutoff by hand). It handles one fixed cutoff per clip. It does not denoise or equalise anything, and it cannot undo clipping or distortion. We tested in Node and Chromium on synthetic voice only. And “SBR” is also the name of a tool inside HE-AAC; this library is not an HE-AAC or MPEG implementation and shares no code with any codec, but we have not done a patent search.

Download

The source is on GitHub: github.com/dialersapps/sbr-crystalizer (issues and pull requests welcome). The downloads below are plain files with nothing to build, no tracking and no outside requests:

The code was written by Dialer's Apps from general signal-processing knowledge; it contains no code copied from another project.