Search code examples
javascripthtmlaudiohtml5-videomedia-player

AudioContext panning audio of playing media


I wonder if is there a way to pan the audio of a video with JavaScript.

The same way you can adjust volume, I need to pan an stereo audio left to right or right to left.

This feature would be useful for multilingual events where you can produce a video in two languages using stereo, for instance, pan english audio to left and german translation to right. Then the player could transform the stereo track into mono muting one of the languages depending on user election.

I already implemented this feature in flash using SoundTransform class http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/media/SoundTransform.html#pan.

I guess SoundTransform html-equivalent is AudioContext http://www.w3.org/TR/webaudio/#AudioContext-section.

May I access the audio context of a playing video?

UPDATE: After some intensive research I found out the solution. Here is some javascript code I used to develop the videojs plugin videjs-stereopanner:

//Init AudioContext
var context = new AudioContext();
var gainL = context.createGainNode();
var gainR = context.createGainNode();
gainL.gain.value = 1;
gainR.gain.value = 1;
var merger = this.context.createChannelMerger(2);
var splitter = this.context.createChannelSplitter(2);

//Connect to source
var source = context.createMediaElementSource(node);
//Connect the source to the splitter
source.connect(splitter, 0, 0);
//Connect splitter' outputs to each Gain Nodes
splitter.connect(gainL, 0);
splitter.connect(gainR, 1);

//Connect Left and Right Nodes to the Merger Node inputs
//Assuming stereo as initial status
gainL.connect(merger, 0, 0);
gainR.connect(merger, 0, 1);

//Connect Merger output to context destination
merger.connect(context.destination, 0, 0);

//Disconnect left channel and connect right to both stereo outputs
var function = panToRight(){
    gainL.disconnect();
    gainR.connect(merger, 0, 1);
};

//Disconnect right channel and connect left to both stereo outputs
var function = panToLeft(){
    gainR.disconnect();
    gainL.connect(merger,0,0);
}

//Restore stereo
var function = panToStereo(){
    gainL.connect(merger, 0, 0);
    gainR.connect(merger, 0, 1);
}

That works for me only in Chrome. If I try to execute this script on iPad/Safari i get an annoying sound which almost deafened me. I'm waiting till Safari implements whole Audio API.


Solution

  • As there is not yet an accepted answer to this I would like to still help you out. At first the planner node from the answer above is not pretty use able, as that calculates the volume and panning depending on a 3 dimensional position, direction and speed in (another) direction. As Mantiur already stated you can use the channelSplitter to get the desired result. You could set it up like so:

    var element = document.getElementById('player');
    Var source = context.createMediaElementSource(element);
    var splitter = context.createSplitter(2);
    source.connect(splitter);
    var left = context.createGain();
    var right = context.createGain();
    splitter.connect(left);
    splitter.connect(right);
    

    You will now be able to connect the left or right node to the context.destination, depending on which of the two the user needs. Please keep in mind that only chrome and Firefox support the web audio api.

    Updated answer: (to the updated question)

    your code looks nice, but it is a lot better to just set the gains for left and right to 0 or 1 rather than disconnect and connect them. With the current issue you will get the one ear problem, but then you'd better not use a merger but just push the audio directly to the destination (or through an extra gain node for setting the final volume. Also notice that your code might work on chrome, but on the version I use this doesn't work, as there a naming issue in context.createGain() and context.createGainNode(). The official document uses .createGain, so we'd better stick to that and create a fallback:

    context.createGain = context.createGain||context.createGainNode;
    

    This might fix the problem in iOS devices, as we should be able to use this there. Despite that MDN is not sure about the compatibility on safari. Sadly there is no workaround to this, due to the behaviour of the web audio API. Lets say the source, which has only one output, but contains two channels inside. There is no other way to split those channels (as at first I was thinking like source.connect(gainL, 0, 0); and source.connect(gainR, 1, 0); but that didn't work due to the numbers being related to the number of in/outputs, not to the channels inside those lines).

    So I recommend change the code to something like this:

    //Init AudioContext
    window.audioContext = window.audioContext||window.webkitAudioContext; //fallback for older chrome browsers
    var context = new AudioContext();
    context.createGain = context.createGain||context.createGainNode; //fallback for gain naming
    var gainL = context.createGain();
    var gainR = context.createGain();
    
    var splitter = this.context.createChannelSplitter(2);
    
    //Connect to source
    var source = context.createMediaElementSource(audioElement);
    //Connect the source to the splitter
    source.connect(splitter, 0, 0);
    //Connect splitter' outputs to each Gain Nodes
    splitter.connect(gainL, 0);
    splitter.connect(gainR, 1);
    
    //Connect Left and Right Nodes to the output
    //Assuming stereo as initial status
    gainL.connect(context.destination, 0);
    gainR.connect(context.destination, 0);
    
    
    //Mute left channel and set the right gain to normal
    function panToRight(){
        gainL.gain.value = 0;
        gainR.gain.value = 1;
    }
    
    //Mute right channel and set the left gain to normal
    function panToLeft(){
        gainL.gain.value = 1;
        gainR.gain.value = 0;
    }
    
    //Restore stereo
    function panToStereo(){
        gainL.gain.value = 1;
        gainR.gain.value = 1;
    }
    

    Oh and btw var function = funcName(){} is not valid javascript (ar are you using some API that changes this behavior)?