1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
|
// Use a power of two to eliminate round-off when converting frames to time and
// vice versa.
let sampleRate = 32768;
let numberOfChannels = 1;
// Time step when each panner node starts. Make sure it starts on a frame
// boundary.
let timeStep = Math.floor(0.001 * sampleRate) / sampleRate;
// 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.1597e-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);
}
|