Web Audio's PannerNode
is supposed to allow placement of the sound source with the positionX
, positionY
and positionZ
parameters, but I can't seem to get things to work consistently.
I've tested things using two different hardware setups: a four-speaker quadraphonic system (created with an aggregate device in Audio/MIDI Setup) and with AirPods Pro, which can mimic 11.1 surround. I've confirmed working surround sound using Dolby Atmos-enabled test videos and surround mixes created in Adobe Audition, so the speakers shouldn't be the problem. I'm using Safari 15.4 on macOS 11.6.4.
In the code below, which I've setup as a JSFiddle here, the sound starts centered but moving even slightly to the left or right moves it all the way to that side. In the quadraphonic setup, everything seems to come from the rear speakers no matter what the settings are. Moving along the Y axis seems to have no affect (expected with the quadraphonic setup, but it should come through on the AirPods).
I'm sure I'm missing something obvious, but I can't seem to figure it out. Can anyone tell me what I'm doing wrong?
var AudioContext = window.AudioContext || window.webkitAudioContext;
var ctx = new AudioContext();
// create a gain node to gate the sound on and off
var g = ctx.createGain();
g.connect(ctx.destination);
g.gain.setValueAtTime(0, ctx.currentTime);
// create a panner to handle sound location
var p = ctx.createPanner();
p.connect(g);
p.positionX.setValueAtTime(0, ctx.currentTime)
// create oscillator to generate the sound
var o = ctx.createOscillator();
o.type = 'sine';
o.frequency.setValueAtTime(440, ctx.currentTime);
o.connect(p);
function startOsc() {
// start oscillator; we "play" the sound by turning volume up and down
o.start(0);
}
function moveSound(x,y,z) {
// move the sound by x, y, and z units
// first, update displayed values (rounding is to avoid floating-point math)
document.querySelector('#xVal').innerHTML = Math.round((p.positionX.value + x)*10)/10;
document.querySelector('#yVal').innerHTML = Math.round((p.positionY.value + y)*10)/10;
document.querySelector('#zVal').innerHTML = Math.round((p.positionZ.value + z)*10)/10;
// now position the sound
p.positionX.value = Math.round((p.positionX.value + x)*10)/10;
p.positionY.value = Math.round((p.positionY.value + y)*10)/10;
p.positionZ.value = Math.round((p.positionZ.value + z)*10)/10;
// now turn volume up and then down 1/2 second later to make a tone
g.gain.setTargetAtTime(1, ctx.currentTime, 0.02);
g.gain.setTargetAtTime(0, ctx.currentTime+0.5, 0.02);
}
<button onclick="startOsc();">start</button>
<button onclick="moveSound(0,0,0);">play in current location</button>
<table>
<tr>
<td><button onclick="moveSound(-0.1,0,0);">move left</button></td>
<td><div id=xVal>0</div></td>
<td><button onclick="moveSound(0.1,0,0);">move right</button></td>
</tr>
<tr>
<td><button onclick="moveSound(0,-0.1,0);">move down</button></td>
<td><div id=yVal>0</div></td>
<td><button onclick="moveSound(0,0.1,0);">move up</button></td>
</tr>
<tr>
<td><button onclick="moveSound(0,0,-0.1);">move backward</button></td>
<td><div id=zVal>0</div></td>
<td><button onclick="moveSound(0,0,0.1);">move forward</button></td>
</tr>
EDIT (3/28/22): After messing with this for a few days, it looks as though spatialization in Web Audio API doesn't actually do anything in a physical surround sound system; it just changes stereo panning and volume to account for the virtual geometry (in my example code above, I did notice that the volume starts to dip when any of the parameters gets as far out as 3 or 4 units). Seems like this could still be introduced at a browser or OS level (after all, Safari can play Dolby Atmos audio in an <audio>
or <video>
tag) but it doesn't look like it's there yet.
EDIT 2 (2/7/23): I eventually got this solved with my own class, which is available here if anyone is interested.