summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webaudio/resources/distance-model-testing.js
blob: f8a6cf940a96f197461f605f1bf527175f63670a (plain)
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
185
186
187
188
189
190
191
192
193
194
195
196
// Use a power of two to eliminate round-off when converting frames to time and
// vice versa.
let sampleRate = 32768;

// How many panner nodes to create for the test.
let nodesToCreate = 100;

// Time step when each panner node starts.  Make sure it starts on a frame
// boundary.
let timeStep = Math.floor(0.001 * sampleRate) / sampleRate;

// 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 = 2.2720e-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);
  }
}