tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

panner-model-testing.js (7515B)


      1 // Use a power of two to eliminate round-off when converting frames to time and
      2 // vice versa.
      3 let sampleRate = 32768;
      4 
      5 let numberOfChannels = 1;
      6 
      7 // Time step when each panner node starts.  Make sure it starts on a frame
      8 // boundary.
      9 let timeStep = Math.floor(0.001 * sampleRate) / sampleRate;
     10 
     11 // Length of the impulse signal.
     12 let pulseLengthFrames = Math.round(timeStep * sampleRate);
     13 
     14 // How many panner nodes to create for the test
     15 let nodesToCreate = 100;
     16 
     17 // Be sure we render long enough for all of our nodes.
     18 let renderLengthSeconds = timeStep * (nodesToCreate + 1);
     19 
     20 // These are global mostly for debugging.
     21 let context;
     22 let impulse;
     23 let bufferSource;
     24 let panner;
     25 let position;
     26 let time;
     27 
     28 let renderedBuffer;
     29 let renderedLeft;
     30 let renderedRight;
     31 
     32 function createGraph(context, nodeCount, positionSetter) {
     33  bufferSource = new Array(nodeCount);
     34  panner = new Array(nodeCount);
     35  position = new Array(nodeCount);
     36  time = new Array(nodeCount);
     37  // Angle between panner locations.  (nodeCount - 1 because we want
     38  // to include both 0 and 180 deg.
     39  let angleStep = Math.PI / (nodeCount - 1);
     40 
     41  if (numberOfChannels == 2) {
     42    impulse = createStereoImpulseBuffer(context, pulseLengthFrames);
     43  } else
     44    impulse = createImpulseBuffer(context, pulseLengthFrames);
     45 
     46  for (let k = 0; k < nodeCount; ++k) {
     47    bufferSource[k] = context.createBufferSource();
     48    bufferSource[k].buffer = impulse;
     49 
     50    panner[k] = context.createPanner();
     51    panner[k].panningModel = 'equalpower';
     52    panner[k].distanceModel = 'linear';
     53 
     54    let angle = angleStep * k;
     55    position[k] = {angle: angle, x: Math.cos(angle), z: Math.sin(angle)};
     56    positionSetter(panner[k], position[k].x, 0, position[k].z);
     57 
     58    bufferSource[k].connect(panner[k]);
     59    panner[k].connect(context.destination);
     60 
     61    // Start the source
     62    time[k] = k * timeStep;
     63    bufferSource[k].start(time[k]);
     64  }
     65 }
     66 
     67 function createTestAndRun(
     68    context, should, nodeCount, numberOfSourceChannels, positionSetter) {
     69  numberOfChannels = numberOfSourceChannels;
     70 
     71  createGraph(context, nodeCount, positionSetter);
     72 
     73  return context.startRendering().then(buffer => checkResult(buffer, should));
     74 }
     75 
     76 async function createTestAndRun_W3CTH(
     77    context, nodeCount, numberOfSourceChannels, positionSetter) {
     78  numberOfChannels = numberOfSourceChannels;
     79  createGraph(context, nodeCount, positionSetter);
     80  const renderedBuffer = await context.startRendering();
     81  return checkResult_W3CTH(renderedBuffer);
     82 }
     83 
     84 // Map our position angle to the azimuth angle (in degrees).
     85 //
     86 // An angle of 0 corresponds to an azimuth of 90 deg; pi, to -90 deg.
     87 function angleToAzimuth(angle) {
     88  return 90 - angle * 180 / Math.PI;
     89 }
     90 
     91 // The gain caused by the EQUALPOWER panning model
     92 function equalPowerGain(angle) {
     93  let azimuth = angleToAzimuth(angle);
     94 
     95  if (numberOfChannels == 1) {
     96    let panPosition = (azimuth + 90) / 180;
     97 
     98    let gainL = Math.cos(0.5 * Math.PI * panPosition);
     99    let gainR = Math.sin(0.5 * Math.PI * panPosition);
    100 
    101    return {left: gainL, right: gainR};
    102  } else {
    103    if (azimuth <= 0) {
    104      let panPosition = (azimuth + 90) / 90;
    105 
    106      let gainL = 1 + Math.cos(0.5 * Math.PI * panPosition);
    107      let gainR = Math.sin(0.5 * Math.PI * panPosition);
    108 
    109      return {left: gainL, right: gainR};
    110    } else {
    111      let panPosition = azimuth / 90;
    112 
    113      let gainL = Math.cos(0.5 * Math.PI * panPosition);
    114      let gainR = 1 + Math.sin(0.5 * Math.PI * panPosition);
    115 
    116      return {left: gainL, right: gainR};
    117    }
    118  }
    119 }
    120 
    121 function checkResult(renderedBuffer, should) {
    122  renderedLeft = renderedBuffer.getChannelData(0);
    123  renderedRight = renderedBuffer.getChannelData(1);
    124 
    125  // The max error we allow between the rendered impulse and the
    126  // expected value.  This value is experimentally determined.  Set
    127  // to 0 to make the test fail to see what the actual error is.
    128  let maxAllowedError = 1.1597e-6;
    129 
    130  let success = true;
    131 
    132  // Number of impulses found in the rendered result.
    133  let impulseCount = 0;
    134 
    135  // Max (relative) error and the index of the maxima for the left
    136  // and right channels.
    137  let maxErrorL = 0;
    138  let maxErrorIndexL = 0;
    139  let maxErrorR = 0;
    140  let maxErrorIndexR = 0;
    141 
    142  // Number of impulses that don't match our expected locations.
    143  let timeCount = 0;
    144 
    145  // Locations of where the impulses aren't at the expected locations.
    146  let timeErrors = new Array();
    147 
    148  for (let k = 0; k < renderedLeft.length; ++k) {
    149    // We assume that the left and right channels start at the same instant.
    150    if (renderedLeft[k] != 0 || renderedRight[k] != 0) {
    151      // The expected gain for the left and right channels.
    152      let pannerGain = equalPowerGain(position[impulseCount].angle);
    153      let expectedL = pannerGain.left;
    154      let expectedR = pannerGain.right;
    155 
    156      // Absolute error in the gain.
    157      let errorL = Math.abs(renderedLeft[k] - expectedL);
    158      let errorR = Math.abs(renderedRight[k] - expectedR);
    159 
    160      if (Math.abs(errorL) > maxErrorL) {
    161        maxErrorL = Math.abs(errorL);
    162        maxErrorIndexL = impulseCount;
    163      }
    164      if (Math.abs(errorR) > maxErrorR) {
    165        maxErrorR = Math.abs(errorR);
    166        maxErrorIndexR = impulseCount;
    167      }
    168 
    169      // Keep track of the impulses that didn't show up where we
    170      // expected them to be.
    171      let expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate);
    172      if (k != expectedOffset) {
    173        timeErrors[timeCount] = {actual: k, expected: expectedOffset};
    174        ++timeCount;
    175      }
    176      ++impulseCount;
    177    }
    178  }
    179 
    180  should(impulseCount, 'Number of impulses found').beEqualTo(nodesToCreate);
    181 
    182  should(
    183      timeErrors.map(x => x.actual),
    184      'Offsets of impulses at the wrong position')
    185      .beEqualToArray(timeErrors.map(x => x.expected));
    186 
    187  should(maxErrorL, 'Error in left channel gain values')
    188      .beLessThanOrEqualTo(maxAllowedError);
    189 
    190  should(maxErrorR, 'Error in right channel gain values')
    191      .beLessThanOrEqualTo(maxAllowedError);
    192 }
    193 
    194 function checkResult_W3CTH(renderedBuffer) {
    195  // The max error we allow between the rendered impulse and the
    196  // expected value.  This value is experimentally determined.  Set
    197  // to 0 to make the test fail to see what the actual error is.
    198  renderedLeft = renderedBuffer.getChannelData(0);
    199  renderedRight = renderedBuffer.getChannelData(1);
    200 
    201  const maxAllowedError = 1.1597e-6;
    202  let impulseCount = 0;
    203  let maxErrorL = 0;
    204  let maxErrorR = 0;
    205  const timeErrors = [];
    206 
    207  for (let k = 0; k < renderedLeft.length; ++k) {
    208    if (renderedLeft[k] !== 0 || renderedRight[k] !== 0) {
    209      const {left: expectedL, right: expectedR} =
    210          equalPowerGain(position[impulseCount].angle);
    211 
    212      maxErrorL = Math.max(maxErrorL, Math.abs(renderedLeft[k] - expectedL));
    213      maxErrorR = Math.max(maxErrorR, Math.abs(renderedRight[k] - expectedR));
    214 
    215      const expectedOffset =
    216          timeToSampleFrame(time[impulseCount], sampleRate);
    217      if (k !== expectedOffset) {
    218        timeErrors.push({actual: k, expected: expectedOffset});
    219      }
    220 
    221      ++impulseCount;
    222    }
    223  }
    224 
    225  assert_equals(
    226      impulseCount,
    227      nodesToCreate,
    228      'Number of impulses found');
    229 
    230  assert_array_equals(
    231      timeErrors.map(e => e.actual),
    232      timeErrors.map(e => e.expected),
    233      'Offsets of impulses at the wrong position');
    234 
    235  assert_less_than_equal(maxErrorL, maxAllowedError,
    236      `Left-channel gain error ${maxErrorL} > ${maxAllowedError}`);
    237  assert_less_than_equal(maxErrorR, maxAllowedError,
    238      `Right-channel gain error ${maxErrorR} > ${maxAllowedError}`);
    239 }