There's something oddly satisfying about generating noise. Not random MP3s — actual algorithmic noise. White noise with a flanger. Distorted static. A growling bass sound made from nothing but random numbers and math. Most people don't have a C compiler and a DSP textbook handy, so I ported an entire audio effects engine from C to JavaScript and wrapped it in a web interface.
The result is a browser-based noise generator with 11 different audio effects — from simple echo to a phase-locked loop that tries to lock onto the chaos. You can tweak four parameters per effect, preview for 3 seconds, or export up to 10 minutes as an MP3. No servers, no uploads, no plugins.
Try it on our free noise generator.
Why Do This in the Browser?
Audio synthesis and DSP (digital signal processing) is traditionally the domain of C, C++, and dedicated audio software. But the Web Audio API and modern JavaScript engines are fast enough to run real-time DSP. Keeping it in the browser means:
- Instant access — no DAW, no VST plugins, no compiler
- Privacy — your noise stays on your machine
- Portability — works on any device with a browser
- Shareability — export an MP3 and send it anywhere
The Architecture: A C-to-JS Port
This isn't a wrapper around Web Audio API nodes. It's a from-scratch port of a C audio processing pipeline. The original code used file I/O (read()/write() system calls) to process raw audio through effect chains. We replaced the file input with Math.random() and the file output with Web Audio API buffers.
The core DSP code lives in three self-written modules:
-
white_noise.js— input generation and I/O translation layer -
audio_effects.js— the full effects engine (980 lines of ported C) -
noise-generator.ts— the React-facing TypeScript wrapper
Generating White Noise
The input to the pipeline isn't a recording — it's pure white noise generated on the fly:
for (let i = 0; i < BLOCKSIZE; i++) {
input[i] = ((Math.random() * 2.0 - 1.0) * 0x7FFFFFFF) | 0;
}
Each sample is a 32-bit signed integer ranging from -0x7FFFFFFF to +0x7FFFFFFF. Using integer math (rather than float) preserves the exact behavior of the original C code, which was written for fixed-point DSP hardware.
The Noise Gate
Before hitting the effect, the signal passes through a noise gate in process_input. This isn't for gating silence — it's a dynamic range tracker that adapts to the signal magnitude:
function process_input(sample) {
if (sample > process_input.max) process_input.max = sample;
if (sample < process_input.min) process_input.min = sample;
magnitude = process_input.max - process_input.min;
// Exponential decay of max/min tracking
process_input.max -= (process_input.max >> 12) + 1;
process_input.min -= (process_input.min >> 12) - 1;
// Adaptive noise gate threshold
if (magnitude >> 22) {
process_input.noise_gate *= 1.001;
if (process_input.noise_gate > max_gate)
process_input.noise_gate = max_gate;
} else {
process_input.noise_gate *= 0.999;
if (process_input.noise_gate < min_gate)
process_input.noise_gate = min_gate;
}
return sample * process_input.noise_gate;
}
The gate has a half-life of roughly 3,000 samples (~60ms at 48kHz). It expands when the signal is loud and contracts when it's quiet, giving the effects a more responsive feel.
Output Clipping
After the effect processes the sample, process_output converts back to 32-bit integers with overflow protection:
function process_output(out) {
let sample = (out * FLOAT_TO_SAMPLE_MULTIPLIER) | 0;
if (out >= 0) {
if (sample < 0) sample = 0x7fffffff;
} else {
if (sample > 0) sample = 0x80000000;
}
return sample;
}
This catches floating-point overflows that would otherwise wrap around and create digital artifacts.
The Effects Engine
The heart of the tool is audio_effects.js — a 980-line port of a C effects framework. It includes shared DSP primitives and 11 distinct effects.
Shared Primitives
Fast Sine/Cosine Lookup
Instead of calling Math.sin() (relatively slow in tight loops), the engine uses a 256-entry quarter-sine table with linear interpolation:
const QUARTER_SINE_STEPS = 256;
const quarter_sin = new Float32Array([/* 257 precomputed values */]);
function fastsincos(phase) {
phase *= 4;
let quadrant = phase | 0;
phase -= quadrant;
phase *= QUARTER_SINE_STEPS;
let idx = phase | 0;
phase -= idx;
let a = quarter_sin[idx];
let b = quarter_sin[idx + 1];
let x = a + (b - a) * phase;
// ...quadrant handling for sin/cos
return { sin: x, cos: y };
}
This is accurate to about 5 digits — more than enough for audio LFOs.
LFO (Low-Frequency Oscillator)
Three waveforms are implemented: sine, triangle, and sawtooth. The LFO phase is stored as a 32-bit unsigned integer that wraps naturally on overflow:
function lfo_step(lfo, type) {
let now = lfo.idx >>> 0;
let next = (now + lfo.step) >>> 0;
lfo.idx = next;
if (type === lfo_sawtooth) return u32_to_fraction(now);
let quarter = now >>> 30;
now = (now << 2) >>> 0;
if (quarter & 1) now = ~now;
if (type === lfo_sinewave) {
let idx = now >>> (32 - QUARTER_SINE_STEP_SHIFT);
let a = quarter_sin[idx];
let b = quarter_sin[idx + 1];
now = (now << QUARTER_SINE_STEP_SHIFT) >>> 0;
return a + (b - a) * u32_to_fraction(now);
} else {
let val = u32_to_fraction(now);
if (quarter & 2) val = -val;
return val;
}
}
The frequency is set by adjusting the step value, which represents how much the 32-bit phase accumulator advances per sample.
Biquad Filters
Second-order IIR filters for low-pass, high-pass, band-pass, notch, and all-pass:
function biquad_step_df1(c, in_, x, y) {
let out = c.b0 * in_ + c.b1 * x[0] + c.b2 * x[1] - c.a1 * y[0] - c.a2 * y[1];
x[1] = x[0]; x[0] = in_;
y[1] = y[0]; y[0] = out;
return out;
}
The coefficients are computed from cutoff frequency and Q factor using standard bilinear transform formulas. These filters are used throughout the effects for tone shaping.
Ring Buffer for Delay Effects
A 65,536-sample circular buffer provides up to ~1.25 seconds of delay at 48kHz:
const SAMPLE_ARRAY_SIZE = 65536;
const SAMPLE_ARRAY_MASK = SAMPLE_ARRAY_SIZE - 1;
const sample_array = new Float32Array(SAMPLE_ARRAY_SIZE);
let sample_array_index = 0;
function sample_array_write(val) {
let idx = SAMPLE_ARRAY_MASK & (++sample_array_index);
sample_array[idx] = val;
}
function sample_array_read(delay) {
let i = delay | 0;
let frac = delay - i;
let idx = sample_array_index - i;
let a = sample_array[SAMPLE_ARRAY_MASK & idx];
let b = sample_array[SAMPLE_ARRAY_MASK & (++idx)];
return a + (b - a) * frac;
}
Linear interpolation on fractional delays prevents zipper noise when the delay time modulates.
The 11 Effects
1. Pure White Noise
No effect. Just the raw noise gate output. Useful for sleep sounds, sound masking, or as a baseline for comparison.
2. Echo
A simple feedback delay line:
function echo_step(in_) {
let d = 1 + effect_delay;
let out = sample_array_read(d);
sample_array_write(limit_value(in_ + out * effect_feedback));
return (in_ + out) / 2;
}
Parameters control delay time (0–1000ms), LFO rate, LFO depth, and feedback amount. High feedback creates endless decaying repeats.
3. Flanger
A modulated delay with feedback. The delay time sweeps sinusoidally, creating the characteristic "jet plane" sweep:
function flanger_step(in_) {
let d = 1 + effect_delay * (1 + lfo_step(effect_lfo, lfo_sinewave) * effect_depth);
let out = sample_array_read(d);
sample_array_write(limit_value(in_ + out * effect_feedback));
return (in_ + out) / 2;
}
4. Phaser
A series of all-pass filters modulated by a triangle LFO. All-pass filters shift phase without changing amplitude; when mixed with the dry signal, they create notches in the spectrum:
function phaser_step(in_) {
let lfo = lfo_step(phaser.lfo, lfo_triangle);
let freq = pow2(lfo * phaser.octaves) * phaser.center_f;
_biquad_allpass_filter(phaser.coeff, freq, phaser.Q);
let out = in_ + phaser.feedback * phaser.s3[0];
out = biquad_step_df1(phaser.coeff, out, phaser.s0, phaser.s1);
out = biquad_step_df1(phaser.coeff, out, phaser.s1, phaser.s2);
out = biquad_step_df1(phaser.coeff, out, phaser.s2, phaser.s3);
return limit_value(in_ + out);
}
Four cascaded all-pass stages create a rich, sweeping texture.
5. Distortion
Three clipping modes with a post-filter tone control:
function distortion_step(in_) {
let driven = in_ * distortion.drive; // 1x to 50x gain
let shaped;
switch (distortion.mode) {
case 0: shaped = soft_clip(driven); break; // x / (1 + |x|)
case 1: shaped = hard_clip(driven); break; // clamp to [-1, 1]
case 2: shaped = asymmetric_clip(driven); break; // different + / -
}
let filtered = biquad_step(distortion.tone_filter, shaped);
return filtered * distortion.level;
}
Soft clip is tube-like. Hard clip is fuzz-pedal aggressive. Asymmetric clip simulates rectifier distortion.
6. Tube
A simplified tube amp emulation using a power-law nonlinearity:
function tube_step(in_) {
in_ *= tube.boost; // 1x to 20x pre-gain
if (in_ + 1 > 0)
in_ = Math.pow(in_ + 1, 1.5) - 1; // Tube warmth curve
else
in_ = -1;
in_ *= tube.volume;
in_ = biquad_step(tube.bass, in_); // High-pass tone control
in_ = biquad_step(tube.treble, in_); // Low-pass tone control
return in_;
}
The pow(x+1, 1.5) - 1 curve mimics the gradual saturation of a vacuum tube.
7. AM (Amplitude Modulation)
A carrier sine wave multiplied by a modulator sine wave:
function am_step(in_) {
let val = lfo_step(am.base_lfo, lfo_sinewave);
let mod = lfo_step(am.mod_lfo, lfo_sinewave);
let multiplier = 1 + mod * am.depth;
return val * multiplier * am.volume;
}
Creates tremolo-like pulsing or metallic ring-modulator sounds depending on the frequency ratio.
8. FM (Frequency Modulation)
A sine wave whose frequency is modulated by another sine wave:
function fm_step(in_) {
let lfo = lfo_step(modulator_lfo, lfo_sinewave);
let multiplier = pow2(lfo * fm_freq_range);
let freq = fm_base_freq * multiplier;
set_lfo_freq(base_lfo, freq);
return lfo_step(base_lfo, lfo_sinewave) * fm_volume;
}
This is classic FM synthesis à la Yamaha DX7, but applied to noise as the modulator source. The result is glassy, bell-like tones emerging from the static.
9. Growling Bass
A complex effect that extracts odd and even harmonics from the input through hard clipping, then mixes them back with a sub-octave component:
function growlingbass_step(in_) {
let filtered_in = biquad_step(growlingbass.lpf_in, in_);
let shaped_odd = hard_clip_growlingbass(filtered_in, previous_minmax);
let shaped_even = Math.abs(in_);
// Sub-octave generation via zero-crossing counting
if ((sign - previous_sign) > 1.0) {
nperiods += 1;
previous_minmax = minmax;
minmax = 0;
}
if (sign > 0.0) {
shaped_sub = (nperiods % 2 === 0) ? filtered_in : -filtered_in;
}
let filtered_odd = biquad_step(growlingbass.lpf_odd, shaped_odd);
let filtered_even = biquad_step(growlingbass.lpf_even, shaped_even);
return shaped_sub * level_sub + in_ + filtered_odd * level_odd + filtered_even * level_even;
}
The sub-octave is generated by flipping polarity every other zero-crossing — a classic analog octave-pedal technique.
10. PLL (Phase-Locked Loop)
The most esoteric effect. It tries to track the dominant frequency in the noise by detecting zero-crossings and using a VCO (voltage-controlled oscillator) to generate a synchronized output:
function pll_step(in_) {
let amplitude = pll_amplitude(in_);
let clean_in = biquad_step(pll.lpf, in_);
// Zero-crossing detection
if (!pll.is_high && clean_in > threshold) {
pll.is_high = 1;
let current_freq = SAMPLES_PER_SEC / pll.samples_since_cross;
if (current_freq > 20.0 && current_freq < 2000.0)
pll.smoothed_freq = linear(0.1, pll.smoothed_freq, current_freq);
pll.samples_since_cross = 0;
}
// VCO tracking
let phase_error = clean_in * cos_out;
let vco_freq = pll.smoothed_freq + (phase_error * pll.smoothed_freq * 0.5);
set_lfo_freq(pll.tracking, vco_freq);
set_lfo_freq(pll.output, vco_freq * 4);
let out = amplitude * lfo_step(pll.output, lfo_triangle);
return linear(pll.blend, in_, out);
}
When it works, it pulls a coherent pitched tone out of random noise. When it doesn't, it produces chaotic, glitchy artifacts. Both are musically interesting.
11. Discontinuous
A granular-style effect that reads from two points in the delay buffer, modulated by a sine LFO:
function discont_step(in_) {
let i = ((disco.lfo.idx << 1) >>> 0) >>> (32 - DISCONT_SHIFT);
let ni = (i + DISCONT_STEPS / 2) & (DISCONT_STEPS - 1);
let sin = lfo_step(disco.lfo, lfo_sinewave);
sample_array_write(in_);
sin *= sin; // Square the sine for smoother crossfade
let d1 = sample_array_read(delay - i * step) * sin;
let d2 = sample_array_read(delay - ni * step) * (1 - sin);
return d1 + d2;
}
The "discontinuous" name comes from the sudden jumps in read position as the LFO sweeps, creating stuttering, fragmented textures.
Exporting to MP3
Once the noise is generated (as a Float32Array interleaved stereo buffer), the export path is the same as the MIDI-to-MP3 tool:
const left = new Int16Array(audioData.length / 2);
const right = new Int16Array(audioData.length / 2);
for (let i = 0; i < audioData.length / 2; i++) {
const l = Math.max(-1, Math.min(1, audioData[i * 2]));
left[i] = l < 0 ? l * 0x8000 : l * 0x7FFF;
const r = Math.max(-1, Math.min(1, audioData[i * 2 + 1]));
right[i] = r < 0 ? r * 0x8000 : r * 0x7FFF;
}
const lame = await loadLamejs();
const encoder = new lame.Mp3Encoder(2, SAMPLE_RATE, 192);
const mp3Data = encoder.encodeBuffer(left, right);
const mp3End = encoder.flush();
const mp3Blob = new Blob([mp3Data, mp3End], { type: 'audio/mp3' });
The loadLamejs function injects lame.min.js via a script tag to avoid bundler issues with the library's implicit global dependencies.
Why Port C Code Instead of Using Web Audio API Nodes?
The Web Audio API has built-in nodes for delay, biquad filters, waveshapers, and oscillators. We could have built the effects chain using those. But porting the C code gives us several advantages:
Exact behavior preservation — The effects sound exactly like the original C implementation, down to the fixed-point quirks and noise gate behavior.
Offline rendering — We can generate any duration of audio without real-time constraints. A 10-minute file renders in seconds, not 10 minutes.
Parameter granularity — Every internal variable is exposed. We can tweak LFO waveforms, filter Q values, and clipping thresholds with full control.
Educational value — The code shows how real DSP algorithms work under the hood, not just how to connect pre-built audio nodes.
Try It Yourself
Need a 10-minute white noise track for sleeping? Want to see what a phase-locked loop does when fed pure static? Curious how FM synthesis sounds when the carrier is random noise?
Head over to our free noise generator. Pick an effect, tweak the knobs, hit Preview, and if you like what you hear, export it as an MP3. All the math happens in your browser — no data leaves your device.

Top comments (0)