Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 3 min read

Pushing Small Speakers to the Limit: Building a Multi-Harmonic Web Audio DSP Chain in Vanilla JavaScript

I spent a few weekends trying to answer a simple question: how loud and clear can a browser make a tiny phone speaker sound, without any library and without distorting? The honest answer surprised me. Turning the gain u

I spent a few weekends trying to answer a simple question: how loud and clear can a browser make a tiny phone speaker sound, without any library and without distorting?

The honest answer surprised me. Turning the gain up does almost nothing useful. What works is shaping the signal so the speaker's energy goes where it can actually move air.

Why a pure sine wave is a bad fit for a phone speaker

A phone speaker is roughly 1 cm across. Below a few hundred hertz it barely moves air, so a pure 165 Hz sine spends most of its energy where the driver can't reproduce it. You hear a weak hum, and pushing the gain only clips the output.

Harmonics help. If I play 165 Hz plus quieter copies at 2Γ—, 3Γ— and 4Γ— (330, 495, 660 Hz), the ear still hears the 165 Hz pitch (the "missing fundamental" effect), and the speaker gets frequencies it can actually play.

const ctx = new AudioContext();
const out = ctx.createGain();

const layers = [
{ mult: 1, gain: 1.0 },
{ mult: 2, gain: 0.55 },
{ mult: 3, gain: 0.3 },
{ mult: 4, gain: 0.16 },
];

function startTone(freq) {
return layers.map(({ mult, gain }) => {
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.frequency.value = freq * mult;
g.gain.value = gain;
osc.connect(g).connect(out);
osc.start();
return osc;
});
}

The chain

Here is the order I ended up with. Each step fixes a problem the step before it creates.

  1. Pre-gain β€” drive the signal hotter on purpose.

  2. WaveShaper (tanh curve) β€” soft-clip the peaks so they round off instead of cracking.

  3. Peaking biquad at 2.8 kHz (+7 dB) β€” small speakers sound clearest around there.

  4. DynamicsCompressor β€” evens out the loud and quiet parts.

  5. Make-up gain β€” gets back the level the compressor took.

  6. Ceiling limiter (about -0.2 dB) β€” a hard stop so nothing goes over full scale.

function softClipCurve(amount = 2.6, n = 2048) {
const curve = new Float32Array(n);
for (let i = 0; i < n; i++) {
const x = (i / (n - 1)) * 2 - 1;
curve[i] = Math.tanh(amount * x) / Math.tanh(amount);
}
return curve;
}

const pre = new GainNode(ctx, { gain: 2.6 });
const shaper = new WaveShaperNode(ctx, { curve: softClipCurve(), oversample: "4x" });
const presence = new BiquadFilterNode(ctx, { type: "peaking", frequency: 2800, Q: 1, gain: 7 });
const comp = new DynamicsCompressorNode(ctx, { threshold: -6, ratio: 12, attack: 0.003, release: 0.1 });
const makeup = new GainNode(ctx, { gain: 1.9 });
const ceiling = new DynamicsCompressorNode(ctx, { threshold: -0.2, ratio: 20, attack: 0.001, release: 0.05 });

out.connect(pre).connect(shaper).connect(presence)
.connect(comp).connect(makeup).connect(ceiling)
.connect(ctx.destination);

oversample: "4x" matters. Without it, the tanh curve adds aliasing that sounds like a gritty buzz.

Loudness compensation for low notes

Our ears are less sensitive to low frequencies (the equal-loudness, or Fletcher–Munson, curves). So I add a small boost that grows as the frequency drops:

function loudnessComp(freq) {
if (freq <= 45) return 1.5;
if (freq <= 70) return 1.38;
if (freq <= 150) return 1.15;
return 1;
}

It's a rough step function, not a calibrated curve, and I'd rather say that up front. It just makes a 40 Hz test tone feel about as loud as a 400 Hz one on a laptop.

Things that bit me

  1. Autoplay rules. Create the AudioContext inside a click handler, or browsers will keep it suspended.

  2. Clicks at start and stop. Ramp the gain over about 20 ms with setTargetAtTime instead of jumping to zero.

  3. Being honest about limits. This chain makes a speaker sound fuller. It can't fix a torn cone or a blown coil, and I don't claim it can.

Where this lives

I use this chain in a small free tool I built, the Speaker Cleaner, which plays low tones and sweeps to shake dust and water out of phone speakers. Everything runs in the browser and nothing is recorded, so you can open DevTools and look at the node graph yourself.

If you've built something similar, I'd like to hear how you handled limiting. I'm still not sure a compressor is the best "brickwall" in Web Audio.

β€” Nayeem, Software Developer

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.