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);
}
|