mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 13:21:05 +00:00
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:
parent
a1bf94501a
commit
2303693b76
@ -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"
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
190
testing/web-platform/tests/webaudio/resources/panner-formulas.js
Normal file
190
testing/web-platform/tests/webaudio/resources/panner-formulas.js
Normal 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};
|
||||
}
|
@ -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);
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
Loading…
Reference in New Issue
Block a user