8

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.

cgcladera
  • 286
  • 2
  • 10

2 Answers2

4

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)?

MarijnS95
  • 4,703
  • 3
  • 23
  • 47
  • Thank you @MarijnS95, that's almost what I was looking for. I've updated my question adding some details about the solution I finally came across. – cgcladera May 09 '14 at 10:15
  • @cgcladera I updated the question including an 'answer' to the iOS problem, although I am not sure. – MarijnS95 May 09 '14 at 14:32
  • Wow! Thank you again @MarijnS95! I'll start from the bottom: Yes I extracted the code sample from an API, so apologies for the incorrect format. Second, there is a reason I connect and disconnect channels. When I pan to right or left I want to be able to listen to the audio in both speakers. That's because I use this feature for multilingual live events and we allow producers to use left and right to stream two languages simultaneously. Therefore, I want the users feel like they can switch language rather than pan audio side to side. – cgcladera May 13 '14 at 14:06
  • @cgcladera That is what I mentioned, if you just connect one of the the mono channels to context.destination it gets automatically converted to stereo, so the use of a merger is superfluous. Has the iOS/safari problem already been fixed? – MarijnS95 May 13 '14 at 16:42
  • 1
    Just after I posted the comment I tried your code and saw what you mean, what a shame!! sorry :( By the way, I'm still having issues with Mac/iOS Safari. The code doesn't work. Although, Safari doesn't trigger exceptions nor errors, it just doesn't behave as expected. – cgcladera May 14 '14 at 10:05
  • @cgcladera are you getting any audio output? What happens if you create some test code without a splitter (but with a gain to test wether the audio passes through the web audio api or that there is a bug at createMediaElement)? – MarijnS95 May 14 '14 at 10:08
  • I am getting audio, actually both left and right at the same time. When I call panRight() or panLeft() it doesn't pan at all in Safari but it does in Chrome. – cgcladera May 14 '14 at 13:42
  • I think there is something wrong with the `createMediaElementSource`, as if the splitter would be broken you won't get any sound. Why? When creating a media element source, the audio element stops playing to the computer output, and instead ports it output to the web audio api. To check this, please test [this](http://jsfiddle.net/MarijnS95/8YYSR/1/) jsfiddle. If you move the slider, the volume should change. If not, it means that safari is not yet capable of using `mediaElementSource`. Of course we can work around that. – MarijnS95 May 14 '14 at 17:15
  • It didn't work either :(. I had to change song source due to Safari doesn't recognise .ogg format. [This](http://jsfiddle.net/8YYSR/2/) is the updated. – cgcladera May 15 '14 at 07:29
  • @cgcladera that means that safari doesn't support createMediaElementSource. There is a way to work around that (I will add that in the answer), but are the media files big? – MarijnS95 May 15 '14 at 08:19
  • Yes, they are often pretty big. – cgcladera May 19 '14 at 08:19
  • 1
    @cgcladera I was considering using [buffers (great example)](http://www.html5rocks.com/en/tutorials/webaudio/intro/) but of course you are using video. Is there a chance you can split the video from the audio? (still that is going to be pretty hard syncing the sound). So Mantriur is right on this, implementation is not yet done, and it is pretty hard to achieve the desired result. I have an exam due in a couple hours, but I might come up with something after that. – MarijnS95 May 19 '14 at 09:20
  • I considered audio splitting as well, but it isn't an option :(. We solved this with server-side transcoding until Safari enhances Audio Context API. – cgcladera May 19 '14 at 13:05
  • What is "node" in line 11? – Mike Cole Jun 06 '14 at 19:38
  • @MikeCole Sharp! It should be an Audio Element. – MarijnS95 Jun 06 '14 at 20:00
3

I added ambient audio on one of my pages that I'm trying to build for a game. Here's my experimentation with panning that I'll be using later on certain sound effects throughout the game. Took forever to realize there was this nice createStereoPanner thing, which simplified the whole process significantly. I tested it with a video and it worked just as well.

var ambientAudio = new Audio('./ambientVideo.mp4');

document.addEventListener('keydown', ambientAudioControl)
function ambientAudioControl(e) {
  if (e.keyCode === 37) panToLeft()
  if (e.keyCode === 39) panToRight()
  if (e.keyCode === 40) panToStereo()
}

const ambientContext = new AudioContext();
const source = ambientContext.createMediaElementSource(ambientAudio);
const ambientPan = ambientContext.createStereoPanner()

function panToLeft(){ ambientPan.pan.value = -1 }
function panToRight(){ ambientPan.pan.value = 1 }
function panToStereo(){ ambientPan.pan.value = 0 }

source.connect(ambientPan)
ambientPan.connect(ambientContext.destination)
ambientAudio.play()
devtanc
  • 136
  • 1
  • 7
  • I originally didn't see your answer, but I settled on something suspiciously similar following the [StereoPannerNode documentation](https://developer.mozilla.org/en-US/docs/Web/API/StereoPannerNode) – Nate Nov 09 '20 at 00:06