Search code examples
javascriptaudioweb-audio-api

Remove audible popping sound when pausing audio with JavaScript


When you pause an audio element with JavaScript using audio.pause(); you can hear an audible pop.

This occurs when a waveform is stopped/cut in the middle of a wave, rather than at a zero-crossing.

I have been looking for a way to resolve this and seen a lot of people suggesting quickly fading out like this:

var fadeAudio = setInterval(function () {
    if (audio.volume <= 0.0) {
        clearInterval(fadeAudio);
        audio.volume = 0;
    }
    if (audio.volume != 0.0){
        audio.volume -= 0.1;
    }
}, 200);

This however will give a worse end result with a descending series of clicks instead.

Edit

I have found this code to add an 'S' curve fade at the beginning of the file but am having difficulty modifying it to fade out a file instead.

var ctx = new AudioContext();
var audio = new Audio('https://samples-files.com/samples/Audio/wav/sample-file-4.wav');
var gainNode = ctx.createGain();
var source = ctx.createBufferSource();

function create_curve_buffer(length, phase) {
    var curve = new Float32Array(length);
    for (var i = 0; i < length; i++) {
        curve[i] = (Math.sin((Math.PI * i / length) - phase)) /2 + 0.5;
    }
    return curve;
};

audio.addEventListener('canplaythrough', function(){
    source.connect(ctx.destination);
    source.connect(gainNode);
    gainNode.connect(ctx.destination);
    
    curve = create_curve_buffer(ctx.sampleRate, (Math.PI/2));
    start = ctx.currentTime;
    
    gainNode.gain.setValueCurveAtTime(curve, start, 5);
});
<button id='play' onClick='audio.play();'>Play</button>
<br />
<button id='stop' onClick='audio.pause();'>Stop</button>

I have basically just tried reversing the curve by doing curve.reverse(); after the for loop and calling the function with the stop button instead of the canplaythrough event.

Then I get an error saying Failed to execute 'setValueCurveAtTime' on 'AudioParam': setValueCurveAtTime(..., 0, 5) overlaps setValueCurveAtTime(..., 0, 5) when I click stop.

What am I doing wrong here?


Solution

  • Final code:

    var audioContext = new (window.AudioContext || window.webkitAudioContext)();
    var source;
    var gainNode = audioContext.createGain();
    var audioBuffer;
    
    async function fetchTrack() {
        var audioData = await fetchAudio("https://samples-files.com/samples/Audio/wav/sample-file-4.wav");
        audioContext.decodeAudioData(audioData, onDecoded, onDecodeError);
    }
    
    function fetchAudio(url) {
        return new Promise((resolve, reject) => {
            var request = new XMLHttpRequest();
            request.open("GET", url, true);
            request.responseType = "arraybuffer";
            request.onload = () => resolve(request.response);
            request.onerror = (e) => reject(e);
            request.send();
        });
    }
    
    function onDecoded(buffer) {
        audioBuffer = buffer;
    }
    
    function onDecodeError(e) {
        console.log("Error decoding buffer: " + e.message);
        console.log(e);
    }
    
    function createCurve(length, phase) {
        var curve = new Float32Array(length);
        for (i = 0; i < length; ++i) {
            curve[i] = (Math.sin((Math.PI * i / length) - phase)) /2 + 0.5;
        }
        curve.reverse();
        return curve;
    };
    
    async function fade(){
        var curve = createCurve(audioContext.sampleRate, (Math.PI/2));
        var start = audioContext.currentTime;
        gainNode.gain.setValueCurveAtTime(curve, start, 1);
        setTimeout(stop,1000);
    }
    
    function stop(){
        source.stop();
    }
    
    function play(){
        source = audioContext.createBufferSource();
        source.buffer = audioBuffer;
        source.connect(gainNode);
        gainNode.connect(audioContext.destination);
        gainNode.gain.setValueAtTime(1, 0)
        audioContext.currentTime = 0;
        source.start();
    }
    
    fetchTrack();
    <button id='play' onClick='play();'>Play</button>
    <br />
    <button id='stop' onClick='fade();'>Stop</button>