Search code examples
javascriptaudioweb-audio-api

Web Audio API gain does not mute and panner does not pan audio completely to one side?


I found some people with similar problems online, especially with regards to gain, but I can't figure out why this is happening, especially since I think I followed the official MDN docs to the T. (this one).

My JS:

// creates the interface in which the entire audio lifecycle graph resides
const audioContext = new AudioContext();

// get the audio element from the DOM
const audioElement = document.querySelector("audio");

// pass it into the audio context as a "source"
const track = audioContext.createMediaElementSource(audioElement);

// connect the input element "track" to the output or destination
track.connect(audioContext.destination);

// get the play button
const playButton = document.querySelector("button");

// add functionality to the play button
playButton.addEventListener("click", (e) => {
    // this is because many browsers "suspend" your audio context unless the user interacts with the page first
    if(audioContext.state === "suspended")
        audioContext.resume();

    // if the audio isn't playing
    if(playButton.dataset.playing === "false") {
        audioElement.play();
        playButton.dataset.playing = "true";
    }
    // if the audio is already playing
    else if(playButton.dataset.playing === "true") {
        audioElement.pause();
        playButton.dataset.playing = "false";
    }
}, false);

// what to do when the audio element finishes playing?
audioElement.addEventListener("ended", (e) => {
    playButton.dataset.playing = "false";
}, false);

// PLAYING AROUND WITH AUDIO GAIN

// create gain node
const gainNode = audioContext.createGain();
// connect it to the audio graph residing in your audioContext
track.connect(gainNode).connect(audioContext.destination);

const volumeControl = document.querySelector("#volume");
volumeControl.addEventListener("input", () => {
    gainNode.gain.value = volumeControl.value;
    console.log(gainNode.gain.value);
}, false);

// PLAYING AROUND WITH THE PANNER

// create panner node
const pannerNode = audioContext.createStereoPanner();
track.connect(gainNode).connect(pannerNode).connect(audioContext.destination);

const pannerControl = document.querySelector("#panner");
pannerControl.addEventListener("input", () => {
    pannerNode.pan.value = pannerControl.value;
    console.log(pannerNode.pan.value);
}, false);

My HTML:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel = "stylesheet" href = "./styles.css">
    <title>Boombox</title>
</head>
<body>
    <audio src = "./music.mp3"></audio>
    <button role = "switch" data-playing = "false" aria-checked = "false">
        <span>Play/Pause</span>
    </button>
    <input type = "range" id = "volume" min = "0" max = "3" value = "1" step = "0.01"/>
    <input type = "range" id = "panner" min = "-1" max = "1" value = "0" step = "0.01"/>
</body>
<script src="script.js"></script>
</html>

I'd appreciate some light on what exactly is happening and why the audio isn't muting or getting panned to one side completely. When I log the values in the console, it reaches 0, or -1 and 1 as expected.


Solution

  • I see that you're connecting track in three separate occasions. Once directly to the output, then through the gain and output, and then through the gain, panner, and output. This means you have three ways the output signal is going to the speakers.

    To fix this, you'll to only use the connection which goes through all the nodes and to the speakers. This will result in a single signal that goes through the gain and panner.

    const audioContext = new AudioContext();
    const audioElement = document.querySelector("audio");
    
    const track = audioContext.createMediaElementSource(audioElement);
    const gainNode = audioContext.createGain();
    const pannerNode = audioContext.createStereoPanner();
    
    const playButton = document.querySelector("button");
    
    track.connect(gainNode).connect(pannerNode).connect(audioContext.destination);
    
    playButton.addEventListener("click", (e) => {
        if(audioContext.state === "suspended")
            audioContext.resume();
    
        if(playButton.dataset.playing === "false") {
            audioElement.play();
            playButton.dataset.playing = "true";
        } else if(playButton.dataset.playing === "true") {
            audioElement.pause();
            playButton.dataset.playing = "false";
        }
    });
    
    audioElement.addEventListener("ended", (e) => {
        playButton.dataset.playing = "false";
    });
    
    const volumeControl = document.querySelector("#volume");
    volumeControl.addEventListener("input", () => {
        gainNode.gain.value = volumeControl.value;
        console.log(gainNode.gain.value);
    });
    
    const pannerControl = document.querySelector("#panner");
    pannerControl.addEventListener("input", () => {
        pannerNode.pan.value = pannerControl.value;
        console.log(pannerNode.pan.value);
    });
    

    I always to try to visualize this API like a guitar with some effect pedals and an amplifier. You connect the wires from the guitar, to the pedals, and then to the amp. That's what the connect method does in my mind.