Advertisement
UI & Interface Sounds

How to Create Blip Sounds with the Web Audio API (Beginner Tutorial)

A beginner-friendly tutorial on generating blip sound effects with the Web Audio API, covering the minimum working code, the autoplay policy that blocks first playback, the parameter ranges that produce a clean digital blip, and how to reuse one function for a full UI sound set.

You have a browser game and you want a button to make a sound when clicked. Not a click sound, not a notification, but a short digital blip that confirms the input was received. You search for how to do this with the Web Audio API, and you find a dozen tutorials that all start with a diagram of the audio graph and a discussion of node types before showing you a single line of working code.

This tutorial does the opposite. It starts with the smallest possible program that produces a blip, and then explains what each line is doing. If you already understand the Web Audio API, the full Web Audio guide for browser games is a better fit. If you are starting from zero and want a working blip in the next ten minutes, this is the article for that.

The blip sound design guide covers the perceptual parameters that make a blip sound like a blip. This article is the code that puts those parameters into a browser page.

What is the shortest Web Audio program that produces a blip?

The answer is six lines of JavaScript. Not six lines of setup, not six lines of configuration. Six lines total, including the function declaration and the closing brace.

function playBlip() {
    const ctx = new AudioContext();
    const osc = ctx.createOscillator();
    osc.type = 'sine';
    osc.frequency.value = 1200;
    osc.connect(ctx.destination);
    osc.start();
    osc.stop(ctx.currentTime + 0.06);
}

That code produces a 60-millisecond sine tone at 1200 Hz. It is not a polished blip — it has a click at the beginning and the end, and it creates a new AudioContext every time it plays — but it is the starting point, and understanding what each line does makes the rest of the article straightforward.

Line by line:

  • new AudioContext() creates the audio environment that all Web Audio nodes live inside. Every sound a page plays goes through one of these.
  • ctx.createOscillator() creates an oscillator node, which is a continuous tone generator. It produces a steady signal until it is told to stop.
  • osc.type = 'sine' sets the waveform. The alternatives are 'square', 'sawtooth', and 'triangle'. For blips, sine or triangle are the right choices; the UI sound design guide covers why.
  • osc.frequency.value = 1200 sets the pitch in Hz. 1200 Hz is in the middle of the range that reads as a digital blip.
  • osc.connect(ctx.destination) connects the oscillator to the speakers. Without this line, the oscillator runs but produces no sound the user can hear.
  • osc.start() and osc.stop(...) start and stop the oscillator. Without the stop call, the tone plays forever.

The two problems with this code — the click and the AudioContext per playback — are the two problems that every other part of this tutorial addresses. The click is fixed by adding an envelope. The AudioContext per playback is fixed by creating one context and reusing it.

Why does the blip not play the first time it is called?

This is the problem that catches every Web Audio beginner. You load the page, you click a button that calls playBlip(), and nothing happens. You click again and the blip plays. The first click is silent.

The cause is the browser's autoplay policy. Every modern browser blocks audio playback until the user has interacted with the page, and the specific mechanism is that the AudioContext is created in a suspended state. The context does not produce sound until resume() is called from within a user gesture handler.

The fix is to resume the context inside the click handler, not on page load. The pattern looks like this:

let audioContext = null;

function getAudioContext() {
    if (!audioContext) {
        audioContext = new AudioContext();
    }
    return audioContext;
}

function resumeAudio() {
    const ctx = getAudioContext();
    if (ctx.state === 'suspended') {
        ctx.resume();
    }
}

// Call resumeAudio() from the first user interaction
document.addEventListener('click', resumeAudio, { once: true });

The { once: true } option means the listener removes itself after the first click. The context only needs to be resumed once, and it stays resumed for the rest of the session.

The reason for the policy is that browsers were overwhelmed with pages that played audio automatically when they loaded, and users found it hostile. The policy is not a bug to work around; it is a design constraint that every Web Audio project has to respect. The coin sound implementation covers the same pattern; the specific requirement is identical for any Web Audio sound effect.

The pattern that works well in a game is a start button on the title screen. The player clicks Start, the handler resumes the context, and every subsequent sound plays normally. This is not just a technical fix; it is also a design pattern, because it gives the player a clear moment of transition between the menu and the game.

How do you add an envelope so the blip does not click?

An oscillator that starts at full amplitude and stops at full amplitude produces a click at both ends of the sound. The click is not part of the blip; it is an artifact of the abrupt transition between silence and sound. Fixing it requires an envelope, which is a gain node whose value changes over time.

The gain node is inserted between the oscillator and the destination, and its gain is ramped up at the start and down at the end. The ramps do not need to be long; a couple of milliseconds at each end is enough to remove the click without affecting the perceived attack or decay of the sound.

function playBlipWithEnvelope() {
    const ctx = getAudioContext();
    const now = ctx.currentTime;

    const osc = ctx.createOscillator();
    const gain = ctx.createGain();

    osc.type = 'sine';
    osc.frequency.value = 1200;

    // Start at zero, ramp up over 3ms, ramp down over the rest
    gain.gain.setValueAtTime(0, now);
    gain.gain.linearRampToValueAtTime(0.8, now + 0.003);
    gain.gain.linearRampToValueAtTime(0, now + 0.06);

    osc.connect(gain);
    gain.connect(ctx.destination);

    osc.start(now);
    osc.stop(now + 0.07);
}

The three gain values are doing three different things. The setValueAtTime(0, now) ensures the sound starts from silence rather than whatever the previous value was. The first ramp brings the gain up to 0.8 over 3 milliseconds — the attack. The second ramp brings it back to zero over the remaining 57 milliseconds — the decay.

The stop time is slightly longer than the envelope's end time (0.07 vs 0.06) so the envelope finishes before the oscillator is stopped. Stopping the oscillator mid-envelope would produce the same click the envelope was supposed to prevent.

The 0.8 peak value is a headroom choice. Web Audio gain values are linear, and 1.0 is the maximum. Producing the blip at 0.8 leaves 20 percent headroom, which prevents the sound from clipping when multiple blips play at the same time or when the blip is combined with other audio. The file size and headroom guide covers the same principle for pre-rendered audio; the reason is identical.

Which parameters produce a clean digital blip?

The parameter ranges below are the starting points for the three most common blip types. The values are the middle of the range that works, not the extremes.

Blip type Frequency Duration Waveform Peak gain
Neutral UI feedback 1000 Hz 60 ms sine 0.7
Positive confirmation 1400 Hz 80 ms sine 0.8
Warning / alert 1800 Hz 70 ms triangle 0.75
Retro / 8-bit 1600 Hz 50 ms square 0.6

The four parameters are the ones that most directly affect how a blip is perceived. The frequency determines the emotional register — low is neutral, high is alerting. The duration determines whether the ear perceives pitch or just a click. The waveform determines how much harmonic content the blip has, which is what makes it sound clean or buzzy. The peak gain determines how loud the blip is relative to everything else in the page.

The waveform is the parameter that most beginners get wrong. A sine wave produces a blip that sounds clean and modern. A square wave produces a blip that sounds harsh and retro. The retro blip is only appropriate when the aesthetic of the page is deliberately retro; for a normal UI, the square wave is too aggressive. If the first attempt at a blip sounds wrong and you cannot identify why, changing the waveform from square to sine is usually the fix. The 8-bit sound design guide covers the cases where the square wave is the right choice.

The frequency range deserves more explanation because it is where the perceptual thresholds show up. A blip below about 800 Hz starts to sound like a musical note rather than a digital signal. A blip above about 2500 Hz becomes piercing, especially on laptop speakers. The range from 800 to 2500 Hz is where the blip reads as "computer making a sound" rather than "music note" or "warning alarm." Within that range, the specific frequency shifts the character but does not change the category.

How do you reuse one function for a whole set of blips?

A project almost always needs more than one blip. Instead of writing a separate function for each, the same function can take parameters. This is the pattern that most tutorials skip, and it is the one that makes the difference between a codebase with one blip and a codebase with a full UI sound set.

function playBlip({ freq = 1000, duration = 0.06,
                     wave = 'sine', gain = 0.7 } = {}) {
    const ctx = getAudioContext();
    const now = ctx.currentTime;

    const osc = ctx.createOscillator();
    const g = ctx.createGain();

    osc.type = wave;
    osc.frequency.value = freq;

    g.gain.setValueAtTime(0, now);
    g.gain.linearRampToValueAtTime(gain, now + 0.003);
    g.gain.linearRampToValueAtTime(0, now + duration);

    osc.connect(g);
    g.connect(ctx.destination);

    osc.start(now);
    osc.stop(now + duration + 0.01);
}

// Now the whole blip set is one call each
playBlip({ freq: 1000, duration: 0.06 });      // neutral
playBlip({ freq: 1400, duration: 0.08, gain: 0.8 }); // confirm
playBlip({ freq: 1800, duration: 0.07, wave: 'triangle' }); // alert
playBlip({ freq: 1600, duration: 0.05, wave: 'square', gain: 0.6 }); // retro

The function signature uses a single options object with defaults, which is the idiomatic pattern for functions that take several parameters. The call sites are readable because each blip specifies only the parameters that differ from the defaults.

The function creates a new oscillator and gain node on every call. This is the standard pattern for short sounds, and it is efficient because the browser's audio engine reclaims the nodes when they finish. For sounds that play many times per second — a machine-gun-style blip — the voice pooling pattern is more appropriate, but for a UI blip that plays when the user clicks a button, the per-call node creation is the right approach.

The pattern also makes it easy to add variation. A blip that plays on every keypress sounds mechanical if every press produces the identical sound. Adding a small random value to the frequency is a one-line change to the function:

const jitter = (Math.random() - 0.5) * 0.04;
osc.frequency.value = freq * (1 + jitter);

The ±2 percent jitter is small enough that the blip still reads as the same sound but large enough that the ear registers each press as a separate event. The variation system guide covers the perceptual thresholds for this kind of variation in more detail.

What should the calling code look like in a real page?

The last piece is wiring the blip function to the actual user interactions. The pattern that works is a single initial click to resume the AudioContext, followed by per-element event listeners that call the blip function.

<!doctype html>
<html>
<body>

<button id="confirm">Confirm</button>
<button id="cancel">Cancel</button>
<button id="alert">Alert</button>

<script>
let audioContext = null;

function getAudioContext() {
    if (!audioContext) audioContext = new AudioContext();
    return audioContext;
}

function playBlip({ freq = 1000, duration = 0.06,
                     wave = 'sine', gain = 0.7 } = {}) {
    const ctx = getAudioContext();
    const now = ctx.currentTime;
    const osc = ctx.createOscillator();
    const g = ctx.createGain();

    osc.type = wave;
    osc.frequency.value = freq;
    g.gain.setValueAtTime(0, now);
    g.gain.linearRampToValueAtTime(gain, now + 0.003);
    g.gain.linearRampToValueAtTime(0, now + duration);
    osc.connect(g);
    g.connect(ctx.destination);
    osc.start(now);
    osc.stop(now + duration + 0.01);
}

// Resume the context on the first interaction
document.addEventListener('click', () => {
    const ctx = getAudioContext();
    if (ctx.state === 'suspended') ctx.resume();
}, { once: true });

// Wire the buttons
document.getElementById('confirm').addEventListener('click',
    () => playBlip({ freq: 1400, duration: 0.08 }));
document.getElementById('cancel').addEventListener('click',
    () => playBlip({ freq: 800, duration: 0.06 }));
document.getElementById('alert').addEventListener('click',
    () => playBlip({ freq: 1800, wave: 'triangle' }));

</script>
</body>
</html>

The order of the two event listeners matters. The document listener that resumes the context fires on any click, including the clicks on the buttons. Because it uses { once: true }, it only fires on the first click. The button listeners fire on every click, and by the time they fire, the context is resumed and the blip plays.

The three buttons demonstrate the three blip types from the earlier table. The Confirm button plays a higher pitch, which reads as positive. The Cancel button plays a lower pitch, which reads as neutral or slightly negative. The Alert button uses a triangle wave, which adds harmonic content and reads as a warning.

The pattern scales to any number of blips. A page with fifteen buttons might use five different blips, and the code stays the same length because the parameters are passed at the call site, not baked into the function.

The Web Audio API is not a library that needs to be learned end to end before producing a useful sound. It is a small set of node types that can be assembled into a working sound effect in a few dozen lines. The tutorial above covers the pieces that a beginner needs to know to produce a blip that works: the oscillator, the gain envelope, the autoplay policy, and the parameter ranges that produce a clean digital tone. Everything else — the panner nodes, the convolution reverbs, the analysers — is for later. A blip is one of the simplest things the API can produce, and it is a good first target because the feedback loop is immediate. Change a parameter, play the blip, hear the difference. That loop is what makes Web Audio learnable, and a blip is a small enough sound that the loop stays fast.

Generate a Reference Blip to Compare Against

Open the SfxMaker blip generator and create a WAV file that follows the parameter ranges from this article. The generated file is a reference for comparing the code output — if the code produces a blip that sounds noticeably different from the reference, one of the parameters is outside the intended range.

Open Blip Sound Generator →
Advertisement