How to Prevent Overlapping Sounds in JavaScript Web Audio API
Why Web Audio Sounds Overlap or Fail to Stop
When building web-based synthesizers or sound effects using JavaScript's Web Audio API, a common trap is creating a new AudioContext on every click or keypress. In the original code, calling oscillator.stop() before declaring the new oscillator triggers a reference error, while removing it allows multiple sound nodes to play concurrently across separate audio contexts.
The Core Issues in the Original Code
- Reference Errors: Calling
oscillator.stop()before the variableoscillatoris created in the event listener scope causes an unhandled JavaScript error. - Multiple Audio Contexts: Re-instantiating
new AudioContext()on every click exhausts system audio instances and prevents proper control over existing nodes. - Scope Isolation: The local
oscillatorvariable loses reference as soon as the click listener function finishes executing.
The Solution: Reusing AudioContext and Storing Node References
To ensure only one tone plays at a time (monophonic behavior), instantiate AudioContext once outside the event listener and store a reference to the active oscillator globally or in an outer scope.
// 1. Create a single AudioContext instance outside event listeners
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let currentOscillator = null;
// Frequency lookup map for cleaner syntax
const frequencies = {
btn1: 200,
btn2: 300,
btn3: 400,
btn4: 500
};
document.getElementById('button-container').addEventListener('click', function(evt) {
const key = evt.target.id;
const frq = frequencies[key];
if (!frq) return; // Exit if clicked element isn't a synth button
// Resume context if suspended by browser autoplay policies
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
// 2. Stop and disconnect the previous oscillator if it exists
if (currentOscillator) {
currentOscillator.stop();
currentOscillator.disconnect();
}
// 3. Create new Oscillator and Gain nodes
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.type = 'triangle';
oscillator.frequency.setValueAtTime(frq, audioCtx.currentTime);
// Prevent clipping by adjusting gain level
gainNode.gain.setValueAtTime(0.2, audioCtx.currentTime);
// 4. Connect nodes to destination
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
// Start the sound and hold reference
oscillator.start();
currentOscillator = oscillator;
});Bonus: Preventing Audio Clicks and Pops
Stopping an oscillator abruptly can cause an audio artifact (a sharp pop or click sound) because the audio waveform is suddenly cut off mid-cycle. To fix this, apply a micro fade-out using gain parameter ramps before stopping the old node:
if (currentGainNode && currentOscillator) {
const now = audioCtx.currentTime;
// Rapid fade out over 10 milliseconds
currentGainNode.gain.setValueAtTime(currentGainNode.gain.value, now);
currentGainNode.gain.linearRampToValueAtTime(0.001, now + 0.01);
// Stop oscillator right after fade out completes
currentOscillator.stop(now + 0.01);
}Summary
Always maintain a single AudioContext throughout your application's lifecycle. By keeping track of active audio nodes in an outer scope variable, you can cleanly stop previous sounds before starting new ones, giving you complete control over web sound playback.