javascriptaudiohtml5-audioweb-audio-apifadeout

Audio fadeout using exponentialRampToValueAtTime in Chrome or Firefox is not reliable


The following code respects the MDN documentation but results in an abrupt mute instead of a 2-second-long fadeout:

const audioContext = new window.AudioContext();
let oscillator;
let gainNode;
document.getElementById("playSweep").addEventListener("click", () => {
    oscillator = audioContext.createOscillator();
    oscillator.type = "sine"; // Sine wave
    oscillator.frequency = 200;
    gainNode = audioContext.createGain();
    gainNode.gain.setValueAtTime(1, audioContext.currentTime);
    oscillator.connect(gainNode);
    gainNode.connect(audioContext.destination);
    oscillator.start();
});
document.getElementById("fadeOut").addEventListener("click", () => {
    gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 2);
});
document.getElementById("fadeOut2").addEventListener("click", () => {
    gainNode.gain.linearRampToValueAtTime(0.001, audioContext.currentTime + 2);
});
<button id="playSweep">Play Frequency Sweep</button>
<button id="fadeOut">Exp Fade Out</button>
<button id="fadeOut2">Lin Fade Out</button>

Even with the linear version, we can hear a click, it's not a clean fadeout.

How to do a proper fadeout in JS Web Audio API?


Solution

  • You (apparently) need to have an active event on the AudioParam when calling these methods for these to transition gracefully. You can achieve this by calling setValueAtTime() right before you call either method.
    Note that a third way to fade-out, which is close enough to exponentialRampToValueAtTime() and allows going to zero, is to use the time constant parameter of setTargetAtTime(target, startTime, timeConstant)

    const audioContext = new window.AudioContext();
    let oscillator;
    let gainNode;
    document.getElementById("playSweep").addEventListener("click", () => {
        oscillator = audioContext.createOscillator();
        oscillator.type = "sine"; // Sine wave
        oscillator.frequency = 200;
        gainNode = audioContext.createGain();
        oscillator.connect(gainNode);
        gainNode.connect(audioContext.destination);
        oscillator.start();
    });
    document.getElementById("fadeOut").addEventListener("click", () => {
        gainNode.gain.setValueAtTime(1, audioContext.currentTime);
        gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 2);
        oscillator.stop(audioContext.currentTime + 2);
    });
    document.getElementById("fadeOut2").addEventListener("click", () => {
        gainNode.gain.setValueAtTime(1, audioContext.currentTime);
        gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 2);
        oscillator?.stop(audioContext.currentTime + 2);
    });
    document.getElementById("fadeOut3").addEventListener("click", () => {
        gainNode.gain.setTargetAtTime(0, audioContext.currentTime, 0.5);
        oscillator?.stop(audioContext.currentTime + 3);
    });
    <button id="playSweep">Play Frequency Sweep</button>
    <button id="fadeOut">Exp Fade Out</button>
    <button id="fadeOut2">Lin Fade Out</button>
    <button id="fadeOut3">Time constant Fade Out</button>