author Marian-Vasile Laza <>
Mon, 20 Mar 2023 21:17:55 +0200
changeset 657270 ef283da5146eca3004d1ab219a5fca8ebb246ef9
parent 469641 ba6f655fd68963530c866d0d4a48c3db3d307777
permissions -rw-r--r--
Backed out changeset 18595f6364ea (bug 1822521) for wpt failures on popover-focus-child-dialog.html. CLOSED TREE

  <title>Test tail time lifetime of PannerNode</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <script type="text/javascript" src="webaudio.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
<pre id="test">
<script class="testbody" type="text/javascript">

// This tests that a PannerNode does not release its reference before
// it finishes emitting sound.
// The PannerNode tail time is short, so, when a PannerNode is destroyed on
// the main thread, it is unlikely to notify the graph thread before the tail
// time expires.  However, by adding DelayNodes downstream from the
// PannerNodes, the graph thread can have enough time to notice that a
// DelayNode has been destroyed.
// In the current implementation, DelayNodes will take a tail-time reference
// immediately when they receive the first block of sound from an upstream
// node, so this test connects the downstream DelayNodes while the upstream
// nodes are finishing, and then runs GC (on the main thread) before the
// DelayNodes receive any input (on the graph thread).
// Web Audio doesn't provide a means to precisely time connect()s but we can
// test that the output of delay nodes matches the output from a reference
// PannerNode that we know will not be GCed.
// Another set of delay nodes is added upstream to ensure that the source node
// has removed its self-reference after dispatching its "ended" event.


const blockSize = 128;
// bufferSize should be long enough that to allow an audioprocess event to be
// sent to the main thread and a connect message to return to the graph
// thread.
const bufferSize = 4096;
const pannerCount = bufferSize / blockSize;
// sourceDelayBufferCount should be long enough to allow the source node
// onended to finish and remove the source self-reference.
const sourceDelayBufferCount = 3;
var gotEnded = false;
// ccDelayLength should be long enough to allow CC to run
var ccDelayBufferCount = 20;
const ccDelayLength = ccDelayBufferCount * bufferSize;

var ctx;
var testPanners = [];
var referencePanner;
var referenceProcessCount = 0;
var referenceOutput = [new Float32Array(bufferSize),
                       new Float32Array(bufferSize)];
var testProcessor;
var testProcessCount = 0;

function isChannelSilent(channel) {
  for (var i = 0; i < channel.length; ++i) {
    if (channel[i] != 0.0) {
      return false;
  return true;

function onReferenceOutput(e) {
  switch(referenceProcessCount) {

  case sourceDelayBufferCount - 1:
    // The panners are about to finish.
    if (!gotEnded) {
      todo(false, "Source hasn't ended.  Increase sourceDelayBufferCount?");

    // Connect each PannerNode output to a downstream DelayNode,
    // and connect ScriptProcessors to compare test and reference panners.
    var delayDuration = ccDelayLength / ctx.sampleRate;
    for (var i = 0; i < pannerCount; ++i) {
      var delay = ctx.createDelay(delayDuration);
      delay.delayTime.value = delayDuration;
    testProcessor = null;
    testPanners = null;

    // The panning effect is linear so only one reference panner is required.
    // This also checks that the individual panners don't chop their output
    // too soon.

    // Assuming the above operations have already scheduled an event to run in
    // stable state and ask the graph thread to make connections, schedule a
    // subsequent event to run cycle collection, which should not collect
    // panners that are still producing sound.
    SimpleTest.executeSoon(function() {


  case sourceDelayBufferCount:
    // Record this buffer during which PannerNode outputs were connected.
    for (var i = 0; i < 2; ++i) {
      e.inputBuffer.copyFromChannel(referenceOutput[i], i);
    } = null;;

    // If the buffer is silent, there is probably not much point just
    // increasing the buffer size, because, with the buffer size already
    // significantly larger than panner tail time, it demonstrates that the
    // lag between threads is much greater than the tail time.
    if (isChannelSilent(referenceOutput[0])) {
      todo(false, "Connections not detected.");


function onTestOutput(e) {
  if (testProcessCount < sourceDelayBufferCount + ccDelayBufferCount) {

  for (var i = 0; i < 2; ++i) {
    compareChannels(e.inputBuffer.getChannelData(i), referenceOutput[i]);
  } = null;;

function startTest() {
  // 0.002 is MaxDelayTimeSeconds in HRTFpanner.cpp
  // and 512 is fftSize() at 48 kHz.
  const expectedPannerTailTime = 0.002 * ctx.sampleRate + 512;

  // Create some PannerNodes downstream from DelayNodes with delays long
  // enough for their source to finish, dispatch its "ended" event
  // and release its playing reference.  The DelayNodes should expire their
  // tail-time references before the PannerNodes and so only the PannerNode
  // lifetimes depends on their tail-time references.  Many DelayNodes are
  // created and timed to finish at different times so that one PannerNode
  // will be finishing the block processed immediately after the connect is
  // received.
  var source = ctx.createBufferSource();
  // Just short of blockSize here to avoid rounding into the next block
  var buffer = ctx.createBuffer(1, blockSize - 1, ctx.sampleRate);
  for (var i = 0; i < buffer.length; ++i) {
    buffer.getChannelData(0)[i] = Math.cos(Math.PI * i / buffer.length);
  source.buffer = buffer;
  source.onended = function(e) {
    gotEnded = true;

  // Time the first test panner to finish just before downstream DelayNodes
  // are about the be connected.  Note that DelayNode lifetime depends on
  // maxDelayTime so set that equal to the delay.
  var delayDuration =
    (sourceDelayBufferCount * bufferSize
     - expectedPannerTailTime - 2 * blockSize) / ctx.sampleRate;

  for (var i = 0; i < pannerCount; ++i) {
    var delay = ctx.createDelay(delayDuration);
    delay.delayTime.value = delayDuration;

    var panner = ctx.createPanner();
    panner.panningModel = "HRTF";
    testPanners[i] = panner;

    delayDuration += blockSize / ctx.sampleRate;

  // Create a ScriptProcessor now to use as a timer to trigger connection of
  // downstream nodes.  It will also be used to record reference output.
  var referenceProcessor = ctx.createScriptProcessor(bufferSize, 2, 0);
  referenceProcessor.onaudioprocess = onReferenceOutput;
  // Start audioprocess events before source delays are connected.

  // The test ScriptProcessor will record output of testPanners. 
  // Create it now so that it is synchronized with the referenceProcessor.
  testProcessor = ctx.createScriptProcessor(bufferSize, 2, 0);
  testProcessor.onaudioprocess = onTestOutput;
  // Start audioprocess events before source delays are connected.

function prepareTest() {
  ctx = new AudioContext();
  // Place the listener to the side of the origin, where the panners are
  // positioned, to maximize delay in one ear.

  // A PannerNode will produce no output until it has loaded its HRIR
  // database.  Wait for this to load before starting the test.
  var processor = ctx.createScriptProcessor(bufferSize, 2, 0);
  referencePanner = ctx.createPanner();
  referencePanner.panningModel = "HRTF";
  var oscillator = ctx.createOscillator();

  processor.onaudioprocess = function(e) {
    if (isChannelSilent(e.inputBuffer.getChannelData(0)))

    referencePanner.disconnect(); = null;