Bug 1442425 [wpt PR 9412] - Upstream PannerNode tests to WPT, a=testonly

Automatic update from web-platform-tests
Bug: 745778
Change-Id: I3b7a5bf5928a412d64cd10eaff729de9dc5a1151
Reviewed-on: https://chromium-review.googlesource.com/903054
Reviewed-by: Hongchan Choi <hongchan@chromium.org>
Commit-Queue: Raymond Toy <rtoy@chromium.org>
Cr-Commit-Position: refs/heads/master@{#534730}

<!-- Reviewable:start -->

<!-- Reviewable:end -->

wpt-commits: b7ee88243f64e6c7f2d00c163bd3bc501e4db7ef
wpt-pr: 9412
reapplied-commits: 370e267e160568862f1fd9ec246ab5bb840f586e, fe4514c84e7ad28e46bad5da93381deb99b177f3, 7806af854343c043a2645a4034fdc7812f65daad, 9ddfd21554293dec5a4bf2e5375ae4f3c9f2ded0, 75f63c4d1ebc949647184fd60972fc7b9fd4affb, 1f3a5b496acd2288cc8cf0c32af86cb35157ea4e, 88b42bd5847abac58a62c4d6b33c1509bfce5f3d, 15c2e4c690700c6c115f8afe5e44ded10d943538, c8d461ef1437641ae7d4ea1d21e1e60cd62910b0, a6088a5f48ee299386a84d2f771902267d7355b1, 0634cd8f08ebe0905a9188fb1398c7b5f889c5dc, c8ee4a012dae506ae06bb5b2ad50942b04c1aaaa, c2c352456a4cf62dcc12f851138b04397675a445, b93a8879555d2fa7e7d4e00a275513a3a6338b35, b86e1331cb36634fd33677043b61fc0c1d8485bc, 44ddf14fd3346658c3223f13652073fafbfa48fa, a1a5840a6bb53e305ba02bcbeb215659342d0edb, 7465cb110ae5ec2e2ca73182caf5293f0efc8fd5, aad5349b3458bc3414e274b33fa86a1123901ff2, eca0907980d2769c449894a6277c60c1a306792f, 38626987c0cfd6e715cfcc6f4f1a1209191a03c5, e4a67f7ddcde6cd99348e9104bd7ed07074da44a, bb3c9990840a0fae2afc840b5952d7874785b112, 042d7adef0bdb9dc80e825c3997ace7519477c42, 99f1ea44fc7915b8b7b33bce4732fa8765fd3ac2
This commit is contained in:
Raymond Toy 2018-03-02 10:47:47 +00:00 committed by moz-wptsync-bot
parent a1bf94501a
commit 2303693b76
15 changed files with 2091 additions and 0 deletions

View File

@ -291380,6 +291380,21 @@
{}
]
],
"webaudio/resources/distance-model-testing.js": [
[
{}
]
],
"webaudio/resources/panner-formulas.js": [
[
{}
]
],
"webaudio/resources/panner-model-testing.js": [
[
{}
]
],
"webaudio/resources/sin_440Hz_-6dBFS_1s.wav": [
[
{}
@ -359450,6 +359465,72 @@
{}
]
],
"webaudio/the-audio-api/the-pannernode-interface/distance-exponential.html": [
[
"/webaudio/the-audio-api/the-pannernode-interface/distance-exponential.html",
{}
]
],
"webaudio/the-audio-api/the-pannernode-interface/distance-inverse.html": [
[
"/webaudio/the-audio-api/the-pannernode-interface/distance-inverse.html",
{}
]
],
"webaudio/the-audio-api/the-pannernode-interface/distance-linear.html": [
[
"/webaudio/the-audio-api/the-pannernode-interface/distance-linear.html",
{}
]
],
"webaudio/the-audio-api/the-pannernode-interface/panner-automation-basic.html": [
[
"/webaudio/the-audio-api/the-pannernode-interface/panner-automation-basic.html",
{}
]
],
"webaudio/the-audio-api/the-pannernode-interface/panner-automation-equalpower-stereo.html": [
[
"/webaudio/the-audio-api/the-pannernode-interface/panner-automation-equalpower-stereo.html",
{}
]
],
"webaudio/the-audio-api/the-pannernode-interface/panner-automation-position.html": [
[
"/webaudio/the-audio-api/the-pannernode-interface/panner-automation-position.html",
{}
]
],
"webaudio/the-audio-api/the-pannernode-interface/panner-distance-clamping.html": [
[
"/webaudio/the-audio-api/the-pannernode-interface/panner-distance-clamping.html",
{}
]
],
"webaudio/the-audio-api/the-pannernode-interface/panner-equalpower-stereo.html": [
[
"/webaudio/the-audio-api/the-pannernode-interface/panner-equalpower-stereo.html",
{}
]
],
"webaudio/the-audio-api/the-pannernode-interface/panner-equalpower.html": [
[
"/webaudio/the-audio-api/the-pannernode-interface/panner-equalpower.html",
{}
]
],
"webaudio/the-audio-api/the-pannernode-interface/panner-rolloff-clamping.html": [
[
"/webaudio/the-audio-api/the-pannernode-interface/panner-rolloff-clamping.html",
{}
]
],
"webaudio/the-audio-api/the-pannernode-interface/pannernode-basic.html": [
[
"/webaudio/the-audio-api/the-pannernode-interface/pannernode-basic.html",
{}
]
],
"webaudio/the-audio-api/the-pannernode-interface/test-pannernode-automation.html": [
[
"/webaudio/the-audio-api/the-pannernode-interface/test-pannernode-automation.html",
@ -592359,6 +592440,18 @@
"1e7c1c4169bc54bd2046ff5b3392f846c6b7b40f",
"support"
],
"webaudio/resources/distance-model-testing.js": [
"0be27d8bddd91a2c4c6e54d3b52af58bff2ed023",
"support"
],
"webaudio/resources/panner-formulas.js": [
"872d9aa271558ee3e7aa293c8e3e44fd525461c0",
"support"
],
"webaudio/resources/panner-model-testing.js": [
"4aa047ffeb14b216d1c329c8959f21e580e33b17",
"support"
],
"webaudio/resources/sin_440Hz_-6dBFS_1s.wav": [
"09c866126524e4fc0d0fae84de8d34419b1a823b",
"support"
@ -592567,6 +592660,50 @@
"da39a3ee5e6b4b0d3255bfef95601890afd80709",
"support"
],
"webaudio/the-audio-api/the-pannernode-interface/distance-exponential.html": [
"c1c94753ebcd1930e326d73c085e6c3197967cd5",
"testharness"
],
"webaudio/the-audio-api/the-pannernode-interface/distance-inverse.html": [
"400d2a373de3e4255279930fca2ec559aed19688",
"testharness"
],
"webaudio/the-audio-api/the-pannernode-interface/distance-linear.html": [
"8e5d7d23f893b7e79446a82f46807b6106ea5643",
"testharness"
],
"webaudio/the-audio-api/the-pannernode-interface/panner-automation-basic.html": [
"2916ba8f03b18c8da0aa71ea37d57bd604b1de81",
"testharness"
],
"webaudio/the-audio-api/the-pannernode-interface/panner-automation-equalpower-stereo.html": [
"43edf7a7bef1f4e9989b048c571659c83ad234f2",
"testharness"
],
"webaudio/the-audio-api/the-pannernode-interface/panner-automation-position.html": [
"81123665ad1f8201907251ddb0bb2707b14925e1",
"testharness"
],
"webaudio/the-audio-api/the-pannernode-interface/panner-distance-clamping.html": [
"820c902b9bcfeed9611ec9c3cba3ee2179a8cee7",
"testharness"
],
"webaudio/the-audio-api/the-pannernode-interface/panner-equalpower-stereo.html": [
"ebe3d2622a8bdc3802159fa0809c1aea28053b09",
"testharness"
],
"webaudio/the-audio-api/the-pannernode-interface/panner-equalpower.html": [
"28120d05419bb478f49f6bb2449fdf282af6624e",
"testharness"
],
"webaudio/the-audio-api/the-pannernode-interface/panner-rolloff-clamping.html": [
"60a763393acf1470fff8b7670b381e28ae6f9270",
"testharness"
],
"webaudio/the-audio-api/the-pannernode-interface/pannernode-basic.html": [
"074d64146ad36b989d783bf93fe295846287538f",
"testharness"
],
"webaudio/the-audio-api/the-pannernode-interface/test-pannernode-automation.html": [
"09241a8acbc3a556ac4fb24c0ae4de8c8f70c2ed",
"testharness"

View File

@ -0,0 +1,193 @@
let sampleRate = 44100.0;
// How many panner nodes to create for the test.
let nodesToCreate = 100;
// Time step when each panner node starts.
let timeStep = 0.001;
// Make sure we render long enough to get all of our nodes.
let renderLengthSeconds = timeStep * (nodesToCreate + 1);
// Length of an impulse signal.
let pulseLengthFrames = Math.round(timeStep * sampleRate);
// Globals to make debugging a little easier.
let context;
let impulse;
let bufferSource;
let panner;
let position;
let time;
// For the record, these distance formulas were taken from the OpenAL
// spec
// (http://connect.creativelabs.com/openal/Documentation/OpenAL%201.1%20Specification.pdf),
// not the code. The Web Audio spec follows the OpenAL formulas.
function linearDistance(panner, x, y, z) {
let distance = Math.sqrt(x * x + y * y + z * z);
distance = Math.min(distance, panner.maxDistance);
let rolloff = panner.rolloffFactor;
let gain =
(1 -
rolloff * (distance - panner.refDistance) /
(panner.maxDistance - panner.refDistance));
return gain;
}
function inverseDistance(panner, x, y, z) {
let distance = Math.sqrt(x * x + y * y + z * z);
distance = Math.min(distance, panner.maxDistance);
let rolloff = panner.rolloffFactor;
let gain = panner.refDistance /
(panner.refDistance + rolloff * (distance - panner.refDistance));
return gain;
}
function exponentialDistance(panner, x, y, z) {
let distance = Math.sqrt(x * x + y * y + z * z);
distance = Math.min(distance, panner.maxDistance);
let rolloff = panner.rolloffFactor;
let gain = Math.pow(distance / panner.refDistance, -rolloff);
return gain;
}
// Map the distance model to the function that implements the model
let distanceModelFunction = {
'linear': linearDistance,
'inverse': inverseDistance,
'exponential': exponentialDistance
};
function createGraph(context, distanceModel, nodeCount) {
bufferSource = new Array(nodeCount);
panner = new Array(nodeCount);
position = new Array(nodeCount);
time = new Array(nodesToCreate);
impulse = createImpulseBuffer(context, pulseLengthFrames);
// Create all the sources and panners.
//
// We MUST use the EQUALPOWER panning model so that we can easily
// figure out the gain introduced by the panner.
//
// We want to stay in the middle of the panning range, which means
// we want to stay on the z-axis. If we don't, then the effect of
// panning model will be much more complicated. We're not testing
// the panner, but the distance model, so we want the panner effect
// to be simple.
//
// The panners are placed at a uniform intervals between the panner
// reference distance and the panner max distance. The source is
// also started at regular intervals.
for (let k = 0; k < nodeCount; ++k) {
bufferSource[k] = context.createBufferSource();
bufferSource[k].buffer = impulse;
panner[k] = context.createPanner();
panner[k].panningModel = 'equalpower';
panner[k].distanceModel = distanceModel;
let distanceStep =
(panner[k].maxDistance - panner[k].refDistance) / nodeCount;
position[k] = distanceStep * k + panner[k].refDistance;
panner[k].setPosition(0, 0, position[k]);
bufferSource[k].connect(panner[k]);
panner[k].connect(context.destination);
time[k] = k * timeStep;
bufferSource[k].start(time[k]);
}
}
// distanceModel should be the distance model string like
// "linear", "inverse", or "exponential".
function createTestAndRun(context, distanceModel, should) {
// To test the distance models, we create a number of panners at
// uniformly spaced intervals on the z-axis. Each of these are
// started at equally spaced time intervals. After rendering the
// signals, we examine where each impulse is located and the
// attenuation of the impulse. The attenuation is compared
// against our expected attenuation.
createGraph(context, distanceModel, nodesToCreate);
return context.startRendering().then(
buffer => checkDistanceResult(buffer, distanceModel, should));
}
// The gain caused by the EQUALPOWER panning model, if we stay on the
// z axis, with the default orientations.
function equalPowerGain() {
return Math.SQRT1_2;
}
function checkDistanceResult(renderedBuffer, model, should) {
renderedData = renderedBuffer.getChannelData(0);
// The max allowed error between the actual gain and the expected
// value. This is determined experimentally. Set to 0 to see
// what the actual errors are.
let maxAllowedError = 3.3e-6;
let success = true;
// Number of impulses we found in the rendered result.
let impulseCount = 0;
// Maximum relative error in the gain of the impulses.
let maxError = 0;
// Array of locations of the impulses that were not at the
// expected location. (Contains the actual and expected frame
// of the impulse.)
let impulsePositionErrors = new Array();
// Step through the rendered data to find all the non-zero points
// so we can find where our distance-attenuated impulses are.
// These are tested against the expected attenuations at that
// distance.
for (let k = 0; k < renderedData.length; ++k) {
if (renderedData[k] != 0) {
// Convert from string to index.
let distanceFunction = distanceModelFunction[model];
let expected =
distanceFunction(panner[impulseCount], 0, 0, position[impulseCount]);
// Adjust for the center-panning of the EQUALPOWER panning
// model that we're using.
expected *= equalPowerGain();
let error = Math.abs(renderedData[k] - expected) / Math.abs(expected);
maxError = Math.max(maxError, Math.abs(error));
should(renderedData[k]).beCloseTo(expected, {threshold: maxAllowedError});
// Keep track of any impulses that aren't where we expect them
// to be.
let expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate);
if (k != expectedOffset) {
impulsePositionErrors.push({actual: k, expected: expectedOffset});
}
++impulseCount;
}
}
should(impulseCount, 'Number of impulses').beEqualTo(nodesToCreate);
should(maxError, 'Max error in distance gains')
.beLessThanOrEqualTo(maxAllowedError);
// Display any timing errors that we found.
if (impulsePositionErrors.length > 0) {
let actual = impulsePositionErrors.map(x => x.actual);
let expected = impulsePositionErrors.map(x => x.expected);
should(actual, 'Actual impulse positions found').beEqualToArray(expected);
}
}

View File

@ -0,0 +1,190 @@
// For the record, these distance formulas were taken from the OpenAL
// spec
// (http://connect.creativelabs.com/openal/Documentation/OpenAL%201.1%20Specification.pdf),
// not the code. The Web Audio spec follows the OpenAL formulas.
function linearDistance(panner, x, y, z) {
let distance = Math.sqrt(x * x + y * y + z * z);
let dref = Math.min(panner.refDistance, panner.maxDistance);
let dmax = Math.max(panner.refDistance, panner.maxDistance);
distance = Math.max(Math.min(distance, dmax), dref);
let rolloff = Math.max(Math.min(panner.rolloffFactor, 1), 0);
if (dref === dmax)
return 1 - rolloff;
let gain = (1 - rolloff * (distance - dref) / (dmax - dref));
return gain;
}
function inverseDistance(panner, x, y, z) {
let distance = Math.sqrt(x * x + y * y + z * z);
distance = Math.max(distance, panner.refDistance);
let rolloff = panner.rolloffFactor;
let gain = panner.refDistance /
(panner.refDistance +
rolloff * (Math.max(distance, panner.refDistance) - panner.refDistance));
return gain;
}
function exponentialDistance(panner, x, y, z) {
let distance = Math.sqrt(x * x + y * y + z * z);
distance = Math.max(distance, panner.refDistance);
let rolloff = panner.rolloffFactor;
let gain = Math.pow(distance / panner.refDistance, -rolloff);
return gain;
}
// Simple implementations of 3D vectors implemented as a 3-element array.
// x - y
function vec3Sub(x, y) {
let z = new Float32Array(3);
z[0] = x[0] - y[0];
z[1] = x[1] - y[1];
z[2] = x[2] - y[2];
return z;
}
// x/|x|
function vec3Normalize(x) {
let mag = Math.hypot(...x);
return x.map(function(c) {
return c / mag;
});
}
// x == 0?
function vec3IsZero(x) {
return x[0] === 0 && x[1] === 0 && x[2] === 0;
}
// Vector cross product
function vec3Cross(u, v) {
let cross = new Float32Array(3);
cross[0] = u[1] * v[2] - u[2] * v[1];
cross[1] = u[2] * v[0] - u[0] * v[2];
cross[2] = u[0] * v[1] - u[1] * v[0];
return cross;
}
// Dot product
function vec3Dot(x, y) {
return x[0] * y[0] + x[1] * y[1] + x[2] * y[2];
}
// a*x, for scalar a
function vec3Scale(a, x) {
return x.map(function(c) {
return a * c;
});
}
function calculateAzimuth(source, listener, listenerForward, listenerUp) {
let sourceListener = vec3Sub(source, listener);
if (vec3IsZero(sourceListener))
return 0;
sourceListener = vec3Normalize(sourceListener);
let listenerRight = vec3Normalize(vec3Cross(listenerForward, listenerUp));
let listenerForwardNorm = vec3Normalize(listenerForward);
let up = vec3Cross(listenerRight, listenerForwardNorm);
let upProjection = vec3Dot(sourceListener, up);
let projectedSource =
vec3Normalize(vec3Sub(sourceListener, vec3Scale(upProjection, up)));
let azimuth =
180 / Math.PI * Math.acos(vec3Dot(projectedSource, listenerRight));
// Source in front or behind the listener
let frontBack = vec3Dot(projectedSource, listenerForwardNorm);
if (frontBack < 0)
azimuth = 360 - azimuth;
// Make azimuth relative to "front" and not "right" listener vector.
if (azimuth >= 0 && azimuth <= 270)
azimuth = 90 - azimuth;
else
azimuth = 450 - azimuth;
// We don't need elevation, so we're skipping that computation.
return azimuth;
}
// Map our position angle to the azimuth angle (in degrees).
//
// An angle of 0 corresponds to an azimuth of 90 deg; pi, to -90 deg.
function angleToAzimuth(angle) {
return 90 - angle * 180 / Math.PI;
}
// The gain caused by the EQUALPOWER panning model
function equalPowerGain(azimuth, numberOfChannels) {
let halfPi = Math.PI / 2;
if (azimuth < -90)
azimuth = -180 - azimuth;
else
azimuth = 180 - azimuth;
if (numberOfChannels == 1) {
let panPosition = (azimuth + 90) / 180;
let gainL = Math.cos(halfPi * panPosition);
let gainR = Math.sin(halfPi * panPosition);
return {left: gainL, right: gainR};
} else {
if (azimuth <= 0) {
let panPosition = (azimuth + 90) / 90;
let gainL = Math.cos(halfPi * panPosition);
let gainR = Math.sin(halfPi * panPosition);
return {left: gainL, right: gainR};
} else {
let panPosition = azimuth / 90;
let gainL = Math.cos(halfPi * panPosition);
let gainR = Math.sin(halfPi * panPosition);
return {left: gainL, right: gainR};
}
}
}
function applyPanner(azimuth, srcL, srcR, numberOfChannels) {
let length = srcL.length;
let outL = new Float32Array(length);
let outR = new Float32Array(length);
if (numberOfChannels == 1) {
for (let k = 0; k < length; ++k) {
let gains = equalPowerGain(azimuth[k], numberOfChannels);
outL[k] = srcL[k] * gains.left;
outR[k] = srcR[k] * gains.right;
}
} else {
for (let k = 0; k < length; ++k) {
let gains = equalPowerGain(azimuth[k], numberOfChannels);
if (azimuth[k] <= 0) {
outL[k] = srcL[k] + srcR[k] * gains.left;
outR[k] = srcR[k] * gains.right;
} else {
outL[k] = srcL[k] * gains.left;
outR[k] = srcR[k] + srcL[k] * gains.right;
}
}
}
return {left: outL, right: outR};
}

View File

@ -0,0 +1,181 @@
let sampleRate = 44100.0;
let numberOfChannels = 1;
// Time step when each panner node starts.
let timeStep = 0.001;
// Length of the impulse signal.
let pulseLengthFrames = Math.round(timeStep * sampleRate);
// How many panner nodes to create for the test
let nodesToCreate = 100;
// Be sure we render long enough for all of our nodes.
let renderLengthSeconds = timeStep * (nodesToCreate + 1);
// These are global mostly for debugging.
let context;
let impulse;
let bufferSource;
let panner;
let position;
let time;
let renderedBuffer;
let renderedLeft;
let renderedRight;
function createGraph(context, nodeCount, positionSetter) {
bufferSource = new Array(nodeCount);
panner = new Array(nodeCount);
position = new Array(nodeCount);
time = new Array(nodeCount);
// Angle between panner locations. (nodeCount - 1 because we want
// to include both 0 and 180 deg.
let angleStep = Math.PI / (nodeCount - 1);
if (numberOfChannels == 2) {
impulse = createStereoImpulseBuffer(context, pulseLengthFrames);
} else
impulse = createImpulseBuffer(context, pulseLengthFrames);
for (let k = 0; k < nodeCount; ++k) {
bufferSource[k] = context.createBufferSource();
bufferSource[k].buffer = impulse;
panner[k] = context.createPanner();
panner[k].panningModel = 'equalpower';
panner[k].distanceModel = 'linear';
let angle = angleStep * k;
position[k] = {angle: angle, x: Math.cos(angle), z: Math.sin(angle)};
positionSetter(panner[k], position[k].x, 0, position[k].z);
bufferSource[k].connect(panner[k]);
panner[k].connect(context.destination);
// Start the source
time[k] = k * timeStep;
bufferSource[k].start(time[k]);
}
}
function createTestAndRun(
context, should, nodeCount, numberOfSourceChannels, positionSetter) {
numberOfChannels = numberOfSourceChannels;
createGraph(context, nodeCount, positionSetter);
return context.startRendering().then(buffer => checkResult(buffer, should));
}
// Map our position angle to the azimuth angle (in degrees).
//
// An angle of 0 corresponds to an azimuth of 90 deg; pi, to -90 deg.
function angleToAzimuth(angle) {
return 90 - angle * 180 / Math.PI;
}
// The gain caused by the EQUALPOWER panning model
function equalPowerGain(angle) {
let azimuth = angleToAzimuth(angle);
if (numberOfChannels == 1) {
let panPosition = (azimuth + 90) / 180;
let gainL = Math.cos(0.5 * Math.PI * panPosition);
let gainR = Math.sin(0.5 * Math.PI * panPosition);
return {left: gainL, right: gainR};
} else {
if (azimuth <= 0) {
let panPosition = (azimuth + 90) / 90;
let gainL = 1 + Math.cos(0.5 * Math.PI * panPosition);
let gainR = Math.sin(0.5 * Math.PI * panPosition);
return {left: gainL, right: gainR};
} else {
let panPosition = azimuth / 90;
let gainL = Math.cos(0.5 * Math.PI * panPosition);
let gainR = 1 + Math.sin(0.5 * Math.PI * panPosition);
return {left: gainL, right: gainR};
}
}
}
function checkResult(renderedBuffer, should) {
renderedLeft = renderedBuffer.getChannelData(0);
renderedRight = renderedBuffer.getChannelData(1);
// The max error we allow between the rendered impulse and the
// expected value. This value is experimentally determined. Set
// to 0 to make the test fail to see what the actual error is.
let maxAllowedError = 1.3e-6;
let success = true;
// Number of impulses found in the rendered result.
let impulseCount = 0;
// Max (relative) error and the index of the maxima for the left
// and right channels.
let maxErrorL = 0;
let maxErrorIndexL = 0;
let maxErrorR = 0;
let maxErrorIndexR = 0;
// Number of impulses that don't match our expected locations.
let timeCount = 0;
// Locations of where the impulses aren't at the expected locations.
let timeErrors = new Array();
for (let k = 0; k < renderedLeft.length; ++k) {
// We assume that the left and right channels start at the same instant.
if (renderedLeft[k] != 0 || renderedRight[k] != 0) {
// The expected gain for the left and right channels.
let pannerGain = equalPowerGain(position[impulseCount].angle);
let expectedL = pannerGain.left;
let expectedR = pannerGain.right;
// Absolute error in the gain.
let errorL = Math.abs(renderedLeft[k] - expectedL);
let errorR = Math.abs(renderedRight[k] - expectedR);
if (Math.abs(errorL) > maxErrorL) {
maxErrorL = Math.abs(errorL);
maxErrorIndexL = impulseCount;
}
if (Math.abs(errorR) > maxErrorR) {
maxErrorR = Math.abs(errorR);
maxErrorIndexR = impulseCount;
}
// Keep track of the impulses that didn't show up where we
// expected them to be.
let expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate);
if (k != expectedOffset) {
timeErrors[timeCount] = {actual: k, expected: expectedOffset};
++timeCount;
}
++impulseCount;
}
}
should(impulseCount, 'Number of impulses found').beEqualTo(nodesToCreate);
should(
timeErrors.map(x => x.actual),
'Offsets of impulses at the wrong position')
.beEqualToArray(timeErrors.map(x => x.expected));
should(maxErrorL, 'Error in left channel gain values')
.beLessThanOrEqualTo(maxAllowedError);
should(maxErrorR, 'Error in right channel gain values')
.beLessThanOrEqualTo(maxAllowedError);
}

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>
distance-exponential.html
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/audit-util.js"></script>
<script src="../../resources/audit.js"></script>
<script src="../../resources/distance-model-testing.js"></script>
</head>
<body>
<script id="layout-test-code">
let audit = Audit.createTaskRunner();
audit.define(
{
label: 'test',
description: 'Exponential distance model for PannerNode'
},
(task, should) => {
// Create offline audio context.
context = new OfflineAudioContext(
2, sampleRate * renderLengthSeconds, sampleRate);
createTestAndRun(context, 'exponential', should)
.then(() => task.done());
});
audit.run();
</script>
</body>
</html>

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title>
distance-inverse.html
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/audit-util.js"></script>
<script src="../../resources/audit.js"></script>
<script src="../../resources/distance-model-testing.js"></script>
</head>
<body>
<script id="layout-test-code">
let audit = Audit.createTaskRunner();
audit.define('test', (task, should) => {
// Create offline audio context.
context = new OfflineAudioContext(
2, sampleRate * renderLengthSeconds, sampleRate);
createTestAndRun(context, 'inverse', should).then(() => task.done());
});
audit.run();
</script>
</body>
</html>

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<title>
distance-linear.html
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/audit-util.js"></script>
<script src="../../resources/audit.js"></script>
<script src="../../resources/distance-model-testing.js"></script>
</head>
<body>
<script id="layout-test-code">
let audit = Audit.createTaskRunner();
audit.define(
{label: 'test', description: 'Linear distance model PannerNode'},
(task, should) => {
// Create offline audio context.
context = new OfflineAudioContext(
2, sampleRate * renderLengthSeconds, sampleRate);
createTestAndRun(context, 'linear', should).then(() => task.done());
});
audit.run();
</script>
</body>
</html>

View File

@ -0,0 +1,298 @@
<!DOCTYPE html>
<html>
<head>
<title>
Test Basic PannerNode with Automation Position Properties
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/audit-util.js"></script>
<script src="../../resources/audit.js"></script>
<script src="../../resources/panner-formulas.js"></script>
</head>
<body>
<script id="layout-test-code">
let sampleRate = 48000;
// These tests are quite slow, so don't run for many frames. 256 frames
// should be enough to demonstrate that automations are working.
let renderFrames = 256;
let renderDuration = renderFrames / sampleRate;
let audit = Audit.createTaskRunner();
// Array of tests for setting the panner positions. These tests basically
// verify that the position setters for the panner and listener are
// working correctly.
let testConfig = [
{
setter: 'positionX',
},
{
setter: 'positionY',
},
{
setter: 'positionZ',
}
];
// Create tests for the panner position setters. Both mono and steroe
// sources are tested.
for (let k = 0; k < testConfig.length; ++k) {
let config = testConfig[k];
// Function to create the test to define the test.
let tester = function(config, channelCount) {
return (task, should) => {
let nodes = createGraph(channelCount);
let {context, source, panner} = nodes;
let message = channelCount == 1 ? 'Mono' : 'Stereo';
message += ' panner.' + config.setter;
testPositionSetter(should, {
nodes: nodes,
pannerSetter: panner[config.setter],
message: message
}).then(() => task.done());
}
};
audit.define('Stereo panner.' + config.setter, tester(config, 2));
audit.define('Mono panner.' + config.setter, tester(config, 1));
}
// Create tests for the listener position setters. Both mono and steroe
// sources are tested.
for (let k = 0; k < testConfig.length; ++k) {
let config = testConfig[k];
// Function to create the test to define the test.
let tester = function(config, channelCount) {
return (task, should) => {
let nodes = createGraph(channelCount);
let {context, source, panner} = nodes;
let message = channelCount == 1 ? 'Mono' : 'Stereo';
message += ' listener.' + config.setter;
// Some relatively arbitrary (non-default) position for the source
// location.
panner.setPosition(1, 0, 1);
testPositionSetter(should, {
nodes: nodes,
pannerSetter: context.listener[config.setter],
message: message
}).then(() => task.done());
}
};
audit.define('Stereo listener.' + config.setter, tester(config, 2));
audit.define('Mono listener.' + config.setter, tester(config, 1));
}
// Test setPosition method.
audit.define('setPosition', (task, should) => {
let {context, panner, source} = createGraph(2);
// Initialize source position (values don't really matter).
panner.setPosition(1, 1, 1);
// After some (unimportant) time, move the panner to a (any) new
// location.
let suspendFrame = 128;
context.suspend(suspendFrame / sampleRate)
.then(function() {
panner.setPosition(-100, 2000, 8000);
})
.then(context.resume.bind(context));
context.startRendering()
.then(function(resultBuffer) {
verifyPannerOutputChanged(
should, resultBuffer,
{message: 'setPosition', suspendFrame: suspendFrame});
})
.then(() => task.done());
});
audit.define('orientation setter', (task, should) => {
let {context, panner, source} = createGraph(2);
// For orientation to matter, we need to make the source directional,
// and also move away from the listener (because the default location is
// 0,0,0).
panner.setPosition(0, 0, 1);
panner.coneInnerAngle = 0;
panner.coneOuterAngle = 360;
panner.coneOuterGain = .001;
// After some (unimportant) time, change the panner orientation to a new
// orientation. The only constraint is that the orientation changes
// from before.
let suspendFrame = 128;
context.suspend(suspendFrame / sampleRate)
.then(function() {
panner.orientationX.value = -100;
panner.orientationY.value = 2000;
panner.orientationZ.value = 8000;
})
.then(context.resume.bind(context));
context.startRendering()
.then(function(resultBuffer) {
verifyPannerOutputChanged(should, resultBuffer, {
message: 'panner.orientation{XYZ}',
suspendFrame: suspendFrame
});
})
.then(() => task.done());
});
audit.define('forward setter', (task, should) => {
let {context, panner, source} = createGraph(2);
// For orientation to matter, we need to make the source directional,
// and also move away from the listener (because the default location is
// 0,0,0).
panner.setPosition(0, 0, 1);
panner.coneInnerAngle = 0;
panner.coneOuterAngle = 360;
panner.coneOuterGain = .001;
// After some (unimportant) time, change the panner orientation to a new
// orientation. The only constraint is that the orientation changes
// from before.
let suspendFrame = 128;
context.suspend(suspendFrame / sampleRate)
.then(function() {
context.listener.forwardX.value = -100;
context.listener.forwardY.value = 2000;
context.listener.forwardZ.value = 8000;
})
.then(context.resume.bind(context));
context.startRendering()
.then(function(resultBuffer) {
verifyPannerOutputChanged(should, resultBuffer, {
message: 'listener.forward{XYZ}',
suspendFrame: suspendFrame
});
})
.then(() => task.done());
});
audit.define('up setter', (task, should) => {
let {context, panner, source} = createGraph(2);
// For orientation to matter, we need to make the source directional,
// and also move away from the listener (because the default location is
// 0,0,0).
panner.setPosition(0, 0, 1);
panner.coneInnerAngle = 0;
panner.coneOuterAngle = 360;
panner.coneOuterGain = .001;
panner.setPosition(1, 0, 1);
// After some (unimportant) time, change the panner orientation to a new
// orientation. The only constraint is that the orientation changes
// from before.
let suspendFrame = 128;
context.suspend(suspendFrame / sampleRate)
.then(function() {
context.listener.upX.value = 100;
context.listener.upY.value = 100;
context.listener.upZ.value = 100;
;
})
.then(context.resume.bind(context));
context.startRendering()
.then(function(resultBuffer) {
verifyPannerOutputChanged(
should, resultBuffer,
{message: 'listener.up{XYZ}', suspendFrame: suspendFrame});
})
.then(() => task.done());
});
audit.run();
function createGraph(channelCount) {
let context = new OfflineAudioContext(2, renderFrames, sampleRate);
let panner = context.createPanner();
let source = context.createBufferSource();
source.buffer =
createConstantBuffer(context, 1, channelCount == 1 ? 1 : [1, 2]);
source.loop = true;
source.connect(panner);
panner.connect(context.destination);
source.start();
return {context: context, source: source, panner: panner};
}
function testPositionSetter(should, options) {
let {nodes, pannerSetter, message} = options;
let {context, source, panner} = nodes;
// Set panner x position. (Value doesn't matter);
pannerSetter.value = 1;
// Wait a bit and set a new position. (Actual time and position doesn't
// matter).
let suspendFrame = 128;
context.suspend(suspendFrame / sampleRate)
.then(function() {
pannerSetter.value = 10000;
})
.then(context.resume.bind(context));
return context.startRendering().then(function(resultBuffer) {
verifyPannerOutputChanged(
should, resultBuffer,
{message: message, suspendFrame: suspendFrame});
});
}
function verifyPannerOutputChanged(should, resultBuffer, options) {
let {message, suspendFrame} = options;
// Verify that the first part of output is constant. (Doesn't matter
// what.)
let data0 = resultBuffer.getChannelData(0);
let data1 = resultBuffer.getChannelData(1);
let middle = '[0, ' + suspendFrame + ') ';
should(
data0.slice(0, suspendFrame),
message + '.value frame ' + middle + 'channel 0')
.beConstantValueOf(data0[0]);
should(
data1.slice(0, suspendFrame),
message + '.value frame ' + middle + 'channel 1')
.beConstantValueOf(data1[0]);
// The rest after suspendTime should be constant and different from the
// first part.
middle = '[' + suspendFrame + ', ' + renderFrames + ') ';
should(
data0.slice(suspendFrame),
message + '.value frame ' + middle + 'channel 0')
.beConstantValueOf(data0[suspendFrame]);
should(
data1.slice(suspendFrame),
message + '.value frame ' + middle + 'channel 1')
.beConstantValueOf(data1[suspendFrame]);
should(
data0[suspendFrame],
message + ': Output at frame ' + suspendFrame + ' channel 0')
.notBeEqualTo(data0[0]);
should(
data1[suspendFrame],
message + ': Output at frame ' + suspendFrame + ' channel 1')
.notBeEqualTo(data1[0]);
}
</script>
</body>
</html>

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<title>
panner-automation-equalpower-stereo.html
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/audit-util.js"></script>
<script src="../../resources/audit.js"></script>
<script src="../../resources/panner-model-testing.js"></script>
</head>
<body>
<script id="layout-test-code">
let audit = Audit.createTaskRunner();
// To test the panner, we create a number of panner nodes
// equally spaced on a semicircle at unit distance. The
// semicircle covers the azimuth range from -90 to 90 deg,
// covering full left to full right. Each source is an impulse
// turning at a different time and we check that the rendered
// impulse has the expected gain.
audit.define(
{
label: 'test',
description:
'Equal-power panner model of AudioPannerNode with stereo source',
},
(task, should) => {
// Create offline audio context.
context = new OfflineAudioContext(
2, sampleRate * renderLengthSeconds, sampleRate);
createTestAndRun(
context, should, nodesToCreate, 2,
function(panner, x, y, z) {
panner.positionX.value = x;
panner.positionY.value = y;
panner.positionZ.value = z;
})
.then(() => task.done());
});
audit.run();
</script>
</body>
</html>

View File

@ -0,0 +1,265 @@
<!DOCTYPE html>
<html>
<head>
<title>
Test Automation of PannerNode Positions
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/audit-util.js"></script>
<script src="../../resources/audit.js"></script>
<script src="../../resources/panner-formulas.js"></script>
</head>
<body>
<script id="layout-test-code">
let sampleRate = 48000;
// These tests are quite slow, so don't run for many frames. 256 frames
// should be enough to demonstrate that automations are working.
let renderFrames = 256;
let renderDuration = renderFrames / sampleRate;
let context;
let panner;
let audit = Audit.createTaskRunner();
// Set of tests for the panner node with automations applied to the
// position of the source.
let testConfigs = [
{
// Distance model parameters for the panner
distanceModel: {model: 'inverse', rolloff: 1},
// Initial location of the source
startPosition: [0, 0, 1],
// Final position of the source. For this test, we only want to move
// on the z axis which
// doesn't change the azimuth angle.
endPosition: [0, 0, 10000],
},
{
distanceModel: {model: 'inverse', rolloff: 1},
startPosition: [0, 0, 1],
// An essentially random end position, but it should be such that
// azimuth angle changes as
// we move from the start to the end.
endPosition: [20000, 30000, 10000],
errorThreshold: [
{
// Error threshold for 1-channel case
relativeThreshold: 4.8124e-7
},
{
// Error threshold for 2-channel case
relativeThreshold: 4.3267e-7
}
],
},
{
distanceModel: {model: 'exponential', rolloff: 1.5},
startPosition: [0, 0, 1],
endPosition: [20000, 30000, 10000],
errorThreshold:
[{relativeThreshold: 5.0783e-7}, {relativeThreshold: 5.2180e-7}]
},
{
distanceModel: {model: 'linear', rolloff: 1},
startPosition: [0, 0, 1],
endPosition: [20000, 30000, 10000],
errorThreshold: [
{relativeThreshold: 6.5324e-6}, {relativeThreshold: 6.5756e-6}
]
}
];
for (let k = 0; k < testConfigs.length; ++k) {
let config = testConfigs[k];
let tester = function(c, channelCount) {
return (task, should) => {
runTest(should, c, channelCount).then(() => task.done());
}
};
let baseTestName = config.distanceModel.model +
' rolloff: ' + config.distanceModel.rolloff;
// Define tasks for both 1-channel and 2-channel
audit.define(k + ': 1-channel ' + baseTestName, tester(config, 1));
audit.define(k + ': 2-channel ' + baseTestName, tester(config, 2));
}
audit.run();
function runTest(should, options, channelCount) {
// Output has 5 channels: channels 0 and 1 are for the stereo output of
// the panner node. Channels 2-5 are the for automation of the x,y,z
// coordinate so that we have actual coordinates used for the panner
// automation.
context = new OfflineAudioContext(5, renderFrames, sampleRate);
// Stereo source for the panner.
let source = context.createBufferSource();
source.buffer = createConstantBuffer(
context, renderFrames, channelCount == 1 ? 1 : [1, 2]);
panner = context.createPanner();
panner.distanceModel = options.distanceModel.model;
panner.rolloffFactor = options.distanceModel.rolloff;
panner.panningModel = 'equalpower';
// Source and gain node for the z-coordinate calculation.
let dist = context.createBufferSource();
dist.buffer = createConstantBuffer(context, 1, 1);
dist.loop = true;
let gainX = context.createGain();
let gainY = context.createGain();
let gainZ = context.createGain();
dist.connect(gainX);
dist.connect(gainY);
dist.connect(gainZ);
// Set the gain automation to match the z-coordinate automation of the
// panner.
// End the automation some time before the end of the rendering so we
// can verify that automation has the correct end time and value.
let endAutomationTime = 0.75 * renderDuration;
gainX.gain.setValueAtTime(options.startPosition[0], 0);
gainX.gain.linearRampToValueAtTime(
options.endPosition[0], endAutomationTime);
gainY.gain.setValueAtTime(options.startPosition[1], 0);
gainY.gain.linearRampToValueAtTime(
options.endPosition[1], endAutomationTime);
gainZ.gain.setValueAtTime(options.startPosition[2], 0);
gainZ.gain.linearRampToValueAtTime(
options.endPosition[2], endAutomationTime);
dist.start();
// Splitter and merger to map the panner output and the z-coordinate
// automation to the correct channels in the destination.
let splitter = context.createChannelSplitter(2);
let merger = context.createChannelMerger(5);
source.connect(panner);
// Split the output of the panner to separate channels
panner.connect(splitter);
// Merge the panner outputs and the z-coordinate output to the correct
// destination channels.
splitter.connect(merger, 0, 0);
splitter.connect(merger, 1, 1);
gainX.connect(merger, 0, 2);
gainY.connect(merger, 0, 3);
gainZ.connect(merger, 0, 4);
merger.connect(context.destination);
// Initialize starting point of the panner.
panner.positionX.setValueAtTime(options.startPosition[0], 0);
panner.positionY.setValueAtTime(options.startPosition[1], 0);
panner.positionZ.setValueAtTime(options.startPosition[2], 0);
// Automate z coordinate to move away from the listener
panner.positionX.linearRampToValueAtTime(
options.endPosition[0], 0.75 * renderDuration);
panner.positionY.linearRampToValueAtTime(
options.endPosition[1], 0.75 * renderDuration);
panner.positionZ.linearRampToValueAtTime(
options.endPosition[2], 0.75 * renderDuration);
source.start();
// Go!
return context.startRendering().then(function(renderedBuffer) {
// Get the panner outputs
let data0 = renderedBuffer.getChannelData(0);
let data1 = renderedBuffer.getChannelData(1);
let xcoord = renderedBuffer.getChannelData(2);
let ycoord = renderedBuffer.getChannelData(3);
let zcoord = renderedBuffer.getChannelData(4);
// We're doing a linear ramp on the Z axis with the equalpower panner,
// so the equalpower panning gain remains constant. We only need to
// model the distance effect.
// Compute the distance gain
let distanceGain = new Float32Array(xcoord.length);
;
if (panner.distanceModel === 'inverse') {
for (let k = 0; k < distanceGain.length; ++k) {
distanceGain[k] =
inverseDistance(panner, xcoord[k], ycoord[k], zcoord[k])
}
} else if (panner.distanceModel === 'linear') {
for (let k = 0; k < distanceGain.length; ++k) {
distanceGain[k] =
linearDistance(panner, xcoord[k], ycoord[k], zcoord[k])
}
} else if (panner.distanceModel === 'exponential') {
for (let k = 0; k < distanceGain.length; ++k) {
distanceGain[k] =
exponentialDistance(panner, xcoord[k], ycoord[k], zcoord[k])
}
}
// Compute the expected result. Since we're on the z-axis, the left
// and right channels pass through the equalpower panner unchanged.
// Only need to apply the distance gain.
let buffer0 = source.buffer.getChannelData(0);
let buffer1 =
channelCount == 2 ? source.buffer.getChannelData(1) : buffer0;
let azimuth = new Float32Array(buffer0.length);
for (let k = 0; k < data0.length; ++k) {
azimuth[k] = calculateAzimuth(
[xcoord[k], ycoord[k], zcoord[k]],
[
context.listener.positionX.value,
context.listener.positionY.value,
context.listener.positionZ.value
],
[
context.listener.forwardX.value,
context.listener.forwardY.value,
context.listener.forwardZ.value
],
[
context.listener.upX.value, context.listener.upY.value,
context.listener.upZ.value
]);
}
let expected = applyPanner(azimuth, buffer0, buffer1, channelCount);
let expected0 = expected.left;
let expected1 = expected.right;
for (let k = 0; k < expected0.length; ++k) {
expected0[k] *= distanceGain[k];
expected1[k] *= distanceGain[k];
}
let info = options.distanceModel.model +
', rolloff: ' + options.distanceModel.rolloff;
let prefix = channelCount + '-channel ' +
'[' + options.startPosition[0] + ', ' + options.startPosition[1] +
', ' + options.startPosition[2] + '] -> [' +
options.endPosition[0] + ', ' + options.endPosition[1] + ', ' +
options.endPosition[2] + ']: ';
let errorThreshold = 0;
if (options.errorThreshold)
errorThreshold = options.errorThreshold[channelCount - 1]
should(data0, prefix + 'distanceModel: ' + info + ', left channel')
.beCloseToArray(expected0, {absoluteThreshold: errorThreshold});
should(data1, prefix + 'distanceModel: ' + info + ', right channel')
.beCloseToArray(expected1, {absoluteThreshold: errorThreshold});
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,233 @@
<!DOCTYPE html>
<html>
<head>
<title>
Test Clamping of Distance for PannerNode
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/audit-util.js"></script>
<script src="../../resources/audit.js"></script>
</head>
<body>
<script id="layout-test-code">
// Arbitrary sample rate and render length.
let sampleRate = 48000;
let renderFrames = 128;
let audit = Audit.createTaskRunner();
audit.define('ref-distance-error', (task, should) => {
testDistanceLimits(should, {name: 'refDistance', isZeroAllowed: true});
task.done();
});
audit.define('max-distance-error', (task, should) => {
testDistanceLimits(should, {name: 'maxDistance', isZeroAllowed: false});
task.done();
});
function testDistanceLimits(should, options) {
// Verify that exceptions are thrown for invalid values of refDistance.
let context = new OfflineAudioContext(1, renderFrames, sampleRate);
let attrName = options.name;
let prefix = 'new PannerNode(c, {' + attrName + ': ';
should(function() {
let nodeOptions = {};
nodeOptions[attrName] = -1;
new PannerNode(context, nodeOptions);
}, prefix + '-1})').throw('RangeError');
if (options.isZeroAllowed) {
should(function() {
let nodeOptions = {};
nodeOptions[attrName] = 0;
new PannerNode(context, nodeOptions);
}, prefix + '0})').notThrow();
} else {
should(function() {
let nodeOptions = {};
nodeOptions[attrName] = 0;
new PannerNode(context, nodeOptions);
}, prefix + '0})').throw('RangeError');
}
// The smallest representable positive single float.
let leastPositiveDoubleFloat = 4.9406564584124654e-324;
should(function() {
let nodeOptions = {};
nodeOptions[attrName] = leastPositiveDoubleFloat;
new PannerNode(context, nodeOptions);
}, prefix + leastPositiveDoubleFloat + '})').notThrow();
prefix = 'panner.' + attrName + ' = ';
panner = new PannerNode(context);
should(function() {
panner[attrName] = -1;
}, prefix + '-1').throw('RangeError');
if (options.isZeroAllowed) {
should(function() {
panner[attrName] = 0;
}, prefix + '0').notThrow();
} else {
should(function() {
panner[attrName] = 0;
}, prefix + '0').throw('RangeError');
}
should(function() {
panner[attrName] = leastPositiveDoubleFloat;
}, prefix + leastPositiveDoubleFloat).notThrow();
}
audit.define('min-distance', (task, should) => {
// Test clamping of panner distance to refDistance for all of the
// distance models. The actual distance is arbitrary as long as it's
// less than refDistance. We test default and non-default values for
// the panner's refDistance and maxDistance.
// correctly.
Promise
.all([
runTest(should, {
distance: 0.01,
distanceModel: 'linear',
}),
runTest(should, {
distance: 0.01,
distanceModel: 'exponential',
}),
runTest(should, {
distance: 0.01,
distanceModel: 'inverse',
}),
runTest(should, {
distance: 2,
distanceModel: 'linear',
maxDistance: 1000,
refDistance: 10,
}),
runTest(should, {
distance: 2,
distanceModel: 'exponential',
maxDistance: 1000,
refDistance: 10,
}),
runTest(should, {
distance: 2,
distanceModel: 'inverse',
maxDistance: 1000,
refDistance: 10,
}),
])
.then(() => task.done());
});
audit.define('max-distance', (task, should) => {
// Like the "min-distance" task, but for clamping to the max
// distance. The actual distance is again arbitrary as long as it is
// greater than maxDistance.
Promise
.all([
runTest(should, {
distance: 20000,
distanceModel: 'linear',
}),
runTest(should, {
distance: 21000,
distanceModel: 'exponential',
}),
runTest(should, {
distance: 23000,
distanceModel: 'inverse',
}),
runTest(should, {
distance: 5000,
distanceModel: 'linear',
maxDistance: 1000,
refDistance: 10,
}),
runTest(should, {
distance: 5000,
distanceModel: 'exponential',
maxDistance: 1000,
refDistance: 10,
}),
runTest(should, {
distance: 5000,
distanceModel: 'inverse',
maxDistance: 1000,
refDistance: 10,
}),
])
.then(() => task.done());
});
function runTest(should, options) {
let context = new OfflineAudioContext(2, renderFrames, sampleRate);
let src = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 20 * 440,
});
// Set panner options. Use a non-default rolloffFactor so that the
// various distance models look distinctly different.
let pannerOptions = {};
Object.assign(pannerOptions, options, {rolloffFactor: 0.5});
let pannerRef = new PannerNode(context, pannerOptions);
let pannerTest = new PannerNode(context, pannerOptions);
// Split the panner output so we can grab just one of the output
// channels.
let splitRef = new ChannelSplitterNode(context, {numberOfOutputs: 2});
let splitTest = new ChannelSplitterNode(context, {numberOfOutputs: 2});
// Merge the panner outputs back into one stereo stream for the
// destination.
let merger = new ChannelMergerNode(context, {numberOfInputs: 2});
src.connect(pannerTest).connect(splitTest).connect(merger, 0, 0);
src.connect(pannerRef).connect(splitRef).connect(merger, 0, 1);
merger.connect(context.destination);
// Move the panner some distance away. Arbitrarily select the x
// direction. For the reference panner, manually clamp the distance.
// All models clamp the distance to a minimum of refDistance. Only the
// linear model also clamps to a maximum of maxDistance.
let xRef = Math.max(options.distance, pannerRef.refDistance);
if (pannerRef.distanceModel === 'linear') {
xRef = Math.min(xRef, pannerRef.maxDistance);
}
let xTest = options.distance;
pannerRef.positionZ.setValueAtTime(xRef, 0);
pannerTest.positionZ.setValueAtTime(xTest, 0);
src.start();
return context.startRendering().then(function(resultBuffer) {
let actual = resultBuffer.getChannelData(0);
let expected = resultBuffer.getChannelData(1);
should(
xTest < pannerRef.refDistance || xTest > pannerRef.maxDistance,
'Model: ' + options.distanceModel + ': Distance (' + xTest +
') is outside the range [' + pannerRef.refDistance + ', ' +
pannerRef.maxDistance + ']')
.beEqualTo(true);
should(actual, 'Test panner output ' + JSON.stringify(options))
.beEqualToArray(expected);
});
}
audit.run();
</script>
</body>
</html>

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<title>
panner-equalpower-stereo.html
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/audit-util.js"></script>
<script src="../../resources/audit.js"></script>
<script src="../../resources/panner-model-testing.js"></script>
</head>
<body>
<script id="layout-test-code">
let audit = Audit.createTaskRunner();
// To test the panner, we create a number of panner nodes
// equally spaced on a semicircle at unit distance. The
// semicircle covers the azimuth range from -90 to 90 deg,
// covering full left to full right. Each source is an impulse
// turning at a different time and we check that the rendered
// impulse has the expected gain.
audit.define(
{
label: 'test',
description:
'Equal-power panner model of AudioPannerNode with stereo source'
},
(task, should) => {
context = new OfflineAudioContext(
2, sampleRate * renderLengthSeconds, sampleRate);
createTestAndRun(
context, should, nodesToCreate, 2,
function(panner, x, y, z) {
panner.setPosition(x, y, z);
})
.then(() => task.done());
});
audit.run();
</script>
</body>
</html>

View File

@ -0,0 +1,139 @@
<!DOCTYPE html>
<html>
<head>
<title>
panner-equalpower.html
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/audit-util.js"></script>
<script src="../../resources/audit.js"></script>
<script src="../../resources/panner-model-testing.js"></script>
</head>
<body>
<script id="layout-test-code">
let audit = Audit.createTaskRunner();
// To test the panner, we create a number of panner nodes
// equally spaced on a semicircle at unit distance. The
// semicircle covers the azimuth range from -90 to 90 deg,
// covering full left to full right. Each source is an impulse
// turning at a different time and we check that the rendered
// impulse has the expected gain.
audit.define(
{
label: 'test',
description: 'Equal-power panner model of AudioPannerNode',
},
(task, should) => {
// Create offline audio context.
context = new OfflineAudioContext(
2, sampleRate * renderLengthSeconds, sampleRate);
createTestAndRun(
context, should, nodesToCreate, 1,
function(panner, x, y, z) {
panner.setPosition(x, y, z);
})
.then(() => task.done());
;
});
// Test that a mono source plays out on both the left and right channels
// when the source and listener positions are the same.
audit.define(
{
label: 'mono source=listener',
description: 'Source and listener at the same position'
},
(task, should) => {
// Must be stereo to verify output and only need a short duration
let context =
new OfflineAudioContext(2, 0.25 * sampleRate, sampleRate);
// Arbitrary position for source and listener. Just so we don't use
// defaults positions.
let x = 1;
let y = 2;
let z = 3;
context.listener.setPosition(x, y, z);
let src = new OscillatorNode(context);
let panner = new PannerNode(context, {
panningModel: 'equalpower',
positionX: x,
positionY: y,
positionZ: z
});
src.connect(panner).connect(context.destination);
src.start();
context.startRendering()
.then(renderedBuffer => {
// Verify that both channels have the same data because they
// should when the source and listener are at the same
// position
let c0 = renderedBuffer.getChannelData(0);
let c1 = renderedBuffer.getChannelData(1);
should(c0, 'Mono: Left and right channels').beEqualToArray(c1);
})
.then(() => task.done());
});
// Test that a stereo source plays out on both the left and right channels
// when the source and listener positions are the same.
audit.define(
{
label: 'stereo source=listener',
description: 'Source and listener at the same position'
},
(task, should) => {
// Must be stereo to verify output and only need a short duration.
let context =
new OfflineAudioContext(2, 0.25 * sampleRate, sampleRate);
// Arbitrary position for source and listener. Just so we don't use
// defaults positions.
let x = 1;
let y = 2;
let z = 3;
context.listener.setPosition(x, y, z);
let src = new OscillatorNode(context);
let merger = new ChannelMergerNode(context, {numberOfInputs: 2});
let panner = new PannerNode(context, {
panningModel: 'equalpower',
positionX: x,
positionY: y,
positionZ: z
});
// Make the oscillator a stereo signal (with identical signals on
// each channel).
src.connect(merger, 0, 0);
src.connect(merger, 0, 1);
merger.connect(panner).connect(context.destination);
src.start();
context.startRendering()
.then(renderedBuffer => {
// Verify that both channels have the same data because they
// should when the source and listener are at the same
// position.
let c0 = renderedBuffer.getChannelData(0);
let c1 = renderedBuffer.getChannelData(1);
should(c0, 'Stereo: Left and right channels').beEqualToArray(c1);
})
.then(() => task.done());
});
audit.run();
</script>
</body>
</html>

View File

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html>
<head>
<title>
Test Clamping of PannerNode rolloffFactor
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/audit-util.js"></script>
<script src="../../resources/audit.js"></script>
</head>
<body>
<script id="layout-test-code">
// Fairly arbitrary sample rate and render frames.
let sampleRate = 16000;
let renderFrames = 2048;
let audit = Audit.createTaskRunner();
audit.define('linear-clamp-low', (task, should) => {
runTest(should, {
distanceModel: 'linear',
// Fairly arbitrary value outside the nominal range
rolloffFactor: -1,
clampedRolloff: 0
}).then(() => task.done());
});
audit.define('linear-clamp-high', (task, should) => {
runTest(should, {
distanceModel: 'linear',
// Fairly arbitrary value outside the nominal range
rolloffFactor: 2,
clampedRolloff: 1
}).then(() => task.done());
});
audit.define('inverse-clamp', (task, should) => {
runTest(should, {
distanceModel: 'inverse',
// Fairly arbitrary value outside the nominal range
rolloffFactor: -1,
clampedRolloff: 0
}).then(() => task.done());
});
audit.define('exponential-clamp', (task, should) => {
runTest(should, {
distanceModel: 'exponential',
// Fairly arbitrary value outside the nominal range
rolloffFactor: -2,
clampedRolloff: 0
}).then(() => task.done());
});
// Test clamping of the rolloffFactor. The test is done by comparing the
// output of a panner with the rolloffFactor set outside the nominal range
// against the output of a panner with the rolloffFactor clamped to the
// nominal range. The outputs should be the same.
//
// The |options| dictionary should contain the members
// distanceModel - The distance model to use for the panners
// rolloffFactor - The desired rolloffFactor. Should be outside the
// nominal range of the distance model.
// clampedRolloff - The rolloffFactor (above) clamped to the nominal
// range for the given distance model.
function runTest(should, options) {
// Offline context with two channels. The first channel is the panner
// node under test. The second channel is the reference panner node.
let context = new OfflineAudioContext(2, renderFrames, sampleRate);
// The source for the panner nodes. This is fairly arbitrary.
let src = new OscillatorNode(context, {type: 'sawtooth'});
// Create the test panner with the specified rolloff factor. The
// position is fairly arbitrary, but something that is not the default
// is good to show the distance model had some effect.
let pannerTest = new PannerNode(context, {
rolloffFactor: options.rolloffFactor,
distanceModel: options.distanceModel,
positionX: 5000
});
// Create the reference panner with the rolloff factor clamped to the
// appropriate limit.
let pannerRef = new PannerNode(context, {
rolloffFactor: options.clampedRolloff,
distanceModel: options.distanceModel,
positionX: 5000
});
// Connect the source to the panners to the destination appropriately.
let merger = new ChannelMergerNode(context, {numberOfInputs: 2});
src.connect(pannerTest).connect(merger, 0, 0);
src.connect(pannerRef).connect(merger, 0, 1);
merger.connect(context.destination);
src.start();
return context.startRendering().then(function(resultBuffer) {
// The two channels should be the same due to the clamping. Verify
// that they are the same.
let actual = resultBuffer.getChannelData(0);
let expected = resultBuffer.getChannelData(1);
let message = 'Panner distanceModel: "' + options.distanceModel +
'", rolloffFactor: ' + options.rolloffFactor;
should(actual, message).beEqualToArray(expected);
});
}
audit.run();
</script>
</body>
</html>

View File

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html>
<head>
<title>
pannernode-basic.html
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/audit-util.js"></script>
<script src="../../resources/audit.js"></script>
</head>
<body>
<script id="layout-test-code">
let context;
let panner;
let audit = Audit.createTaskRunner();
audit.define('initialize', (task, should) => {
should(() => {
context = new AudioContext();
panner = context.createPanner();
}, 'Initialize context and panner').notThrow();
task.done();
});
audit.define('basic', (task, should) => {
should(panner.numberOfInputs, 'panner.numberOfInputs').beEqualTo(1);
should(panner.numberOfOutputs, 'panner.numberOfOutputs').beEqualTo(1);
should(panner.refDistance, 'panner.refDistance').beEqualTo(1);
panner.refDistance = 270.5;
should(panner.refDistance, 'panner.refDistance = 270.5')
.beEqualTo(270.5);
should(panner.maxDistance, 'panner.maxDistance').beEqualTo(10000);
panner.maxDistance = 100.5;
should(panner.maxDistance, 'panner.maxDistance = 100.5')
.beEqualTo(100.5);
should(panner.rolloffFactor, 'panner.rolloffFactor').beEqualTo(1);
panner.rolloffFactor = 0.75;
should(panner.rolloffFactor, 'panner.rolloffFactor = 0.75')
.beEqualTo(0.75);
should(panner.coneInnerAngle, 'panner.coneInnerAngle').beEqualTo(360);
panner.coneInnerAngle = 240.5;
should(panner.coneInnerAngle, 'panner.coneInnerAngle = 240.5')
.beEqualTo(240.5);
should(panner.coneOuterAngle, 'panner.coneOuterAngle').beEqualTo(360);
panner.coneOuterAngle = 166.5;
should(panner.coneOuterAngle, 'panner.coneOuterAngle = 166.5')
.beEqualTo(166.5);
should(panner.coneOuterGain, 'panner.coneOuterGain').beEqualTo(0);
panner.coneOuterGain = 0.25;
should(panner.coneOuterGain, 'panner.coneOuterGain = 0.25')
.beEqualTo(0.25);
should(panner.panningModel, 'panner.panningModel')
.beEqualTo('equalpower');
should(panner.distanceModel)
.beEqualTo('inverse', 'panner.distanceModel');
should(panner.positionX.value, 'panner.positionX').beEqualTo(0);
should(panner.positionY.value, 'panner.positionY').beEqualTo(0);
should(panner.positionZ.value, 'panner.positionZ').beEqualTo(0);
should(panner.orientationX.value, 'panner.orientationX').beEqualTo(1);
should(panner.orientationY.value, 'panner.orientationY').beEqualTo(0);
should(panner.orientationZ.value, 'panner.orientationZ').beEqualTo(0);
task.done();
});
audit.define('listener', (task, should) => {
should(context.listener.positionX.value, 'listener.positionX')
.beEqualTo(0);
should(context.listener.positionY.value, 'listener.positionY')
.beEqualTo(0);
should(context.listener.positionZ.value, 'listener.positionZ')
.beEqualTo(0);
should(context.listener.forwardX.value, 'listener.forwardX')
.beEqualTo(0);
should(context.listener.forwardY.value, 'listener.forwardY')
.beEqualTo(0);
should(context.listener.forwardZ.value, 'listener.forwardZ')
.beEqualTo(-1);
should(context.listener.upX.value, 'listener.upX').beEqualTo(0);
should(context.listener.upY.value, 'listener.upY').beEqualTo(1);
should(context.listener.upZ.value, 'listener.upZ').beEqualTo(0);
task.done();
});
audit.define('panning models', (task, should) => {
// Check that the .panningModel attribute can be set to all legal
// values.
let panningModels = ['equalpower', 'HRTF'];
for (let i = 0; i < panningModels.length; ++i) {
should(function() {
panner.panningModel = panningModels[i];
}, 'Set panner.panningModel = "' + panningModels[i] + '"').notThrow();
should(
panner.panningModel,
'panner.panningModel = "' + panningModels[i] + '"')
.beEqualTo(panningModels[i]);
}
should(function() {
panner.panningModel = 'invalid';
}, 'panner.panningModel = "invalid"').notThrow();
should(panner.panningModel, 'panner.panningModel after invalid setter')
.beEqualTo('HRTF');
// Check that numerical values are no longer supported. We shouldn't
// throw and the value shouldn't be changed.
panner.panningModel = 'HRTF';
should(function() {
panner.panningModel = 1;
}, 'panner.panningModel = 1').notThrow();
should(panner.panningModel, 'panner.panningModel').beEqualTo('HRTF');
task.done();
});
audit.define('distance models', (task, should) => {
// Check that the .panningModel attribute can be set to all legal
// values.
let distanceModels = ['linear', 'inverse', 'exponential'];
for (let i = 0; i < distanceModels.length; ++i) {
should(function() {
panner.distanceModel = distanceModels[i];
}, 'panner.distanceModel = "' + distanceModels[i] + '"').notThrow();
should(
panner.distanceModel,
'panner.distanceModel = "' + distanceModels[i] + '"')
.beEqualTo(distanceModels[i]);
}
should(function() {
panner.distanceModel = 'invalid';
}, 'panner.distanceModel = "invalid"').notThrow();
should(panner.distanceModel, 'panner.distanceModel')
.beEqualTo('exponential');
task.done();
});
audit.run();
</script>
</body>
</html>