summaryrefslogtreecommitdiffstats
path: root/dom/media/webaudio/test/test_pannerNodeTail.html
blob: 1f6483b581a4db734ef7265fbadf7e9a8765961f (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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
<!DOCTYPE HTML>
<html>
<head>
  <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" />
</head>
<body>
<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.

SimpleTest.waitForExplicitFinish();

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;
      delay.connect(testProcessor);
      testPanners[i].connect(delay);
    }
    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.
    referencePanner.connect(e.target);

    // 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() {
      SpecialPowers.forceGC();
      SpecialPowers.forceCC();
    });

    break;

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

    // 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.");
    }
  }

  referenceProcessCount++;
}

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

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

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.start(0);
  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;
    source.connect(delay);
    delay.connect(referencePanner)

    var panner = ctx.createPanner();
    panner.panningModel = "HRTF";
    delay.connect(panner);
    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.
  referenceProcessor.connect(ctx.destination);

  // 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.
  testProcessor.connect(ctx.destination);
}

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.
  ctx.listener.setPosition(1,0,0);

  // 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";
  referencePanner.connect(processor);
  var oscillator = ctx.createOscillator();
  oscillator.connect(referencePanner);
  oscillator.start(0);

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

    oscillator.stop(0);
    oscillator.disconnect();
    referencePanner.disconnect();
    e.target.onaudioprocess = null;
    SimpleTest.executeSoon(startTest);
  };
}
prepareTest();
</script>
</pre>
</body>
</html>